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

自定义下拉刷新和加载更多_Android

2014-08-14 17:53 429 查看
在一个外包项目,在我负责的模块里头刚好要频繁的用到下拉刷新和加载更多的功能,由于自己项目的需求和业务处理,于是乎就自己写了一个刷新和加载更多的框架并且做了一些相应的优化,基本上每个app都有刷新和加载更多的功能,在开源库上面也有不少大神发布了自己开源项目其中就有pulltoRefresh,很多csdn的牛牛们也都有自己的开源框架,今天看到这么多牛牛们的文章,实在是刺激到我了,所以最近脑子一热也作死的写了一个自己的下拉刷新和加载更多的框架,敲好今天没事就打算写写博客和给大家分tu享cao下

接来来将会按照以下几块进行讲解

1.UI实现(刷新头部的隐藏:滑动监听,属性动画,view的测绘)

2.下拉刷新和加载更多的实现刷新的回调接口

3.加载更多的实现

1.下拉刷新的UI实现

先上图吧



环境:要将下拉刷新加在带自定义头的一个listView中,现在划分为四个部分:头布局,刷新部分,自定义部分,listView的item部分; 

首先来说一说自定义view呢主要无非就用到:

  1.touch:主要就是用户与手机的交互

  2.View绘制方面:大小控制,布局控制,绘制的控制,用户行为(touch)

因为下拉刷新和加载更多主要是处理用户行为的,所以下面主要就是围绕着用户行为(touch)来说

现在开始进入正题:因为listView有addHeader()和addFooterView()方法刚好可以用来加载下来刷新部分的刷新头和加载更多的那个加载更多的部分.那么怎么控制刷新头和加载更多那个尾巴部分的动态拉出和放回呢?他又是怎么实现的呢?

在android中提供了一个api:setPadding(0,TOP,0,0);没错就是这个setPadding()方法,

思想:

通过这个三个图不难看出:当这个PaddingTop值等于0时敲好刷新头恰好完全显示,paddingTop=正数时,正多少刷新头就向下偏移多少,当paddingTop=负数值是,负多少刷新头就往上偏移多少.

好啦现在知道这个原理后,只要通过touch的触摸上下滑动记录滑动的值,动态的改变paddingTop的值就是可以控制刷新头的隐藏和显示了.

好了,现在总结下相思,首先通过addHeader()将刷新头给添加进来,再结合setPadding(0,Top,0,0)方法传进去Top的值来控制刷新头的位置,最后再通过toch触摸的记录把滑动屏幕的值动态传给Top就可以了
//隐藏 刷新部分的头部布局,利用 paddingTop 的方法将需要隐藏的那个 头布局 设置为它的 高度的 负数
mRefreshView.measure(0, 0);//这个方法是为了找到这个控件在,layout中,这样再使用getMeasuredHeight()才有值
mRefreshHeight = mRefreshView.getMeasuredHeight();
Log.d(TAG, "刷新部分的头布局的高度:" + mRefreshHeight);
mHeaderLayout.setPadding(0, -mRefreshHeight, 0, 0);


Ps:这里有个问题 那么这个刷新头的高度是多少呢?有人可能会说,这个很简单啊,我在xml布局的时候把这个刷新头的高度给限定死不就行了嘛,直接传进去一个dp值就好啦.要是这样做的话,又会面临一个新的问题,那你手机屏幕的适配呢?天底下那么多屏幕大小不同的手机那不就乱了套了吗?google的设计者已经考虑到这个问题了,直接通过getMeasuredHeight()的方法就可以获得该刷新头的高度了,这样就变得灵活了.

细心的朋友可能注意到了,mRefreshView.measure(0,0);//这个测绘传进去两个0 0 ,这不是一点意义都没有吗?能否把这句代码去掉呢,答案是否定的,先让你的刷新头mRefreshView调用这个measure(0,0)是有用意的,这句代码的意义就好比先把你要测绘高度的view先做初始化,也就是先找到它,要是没有这句话,getMeasuredHeight()得到的是一个0值.

现在能控制刷新头的显示和隐藏了,已经迈出了关键的一步了,别急后面还有几步呢.现在你只是把一个view上下拉动它,但是它和我们平时用的不太一样.它没有实现动画呀(下拉刷新箭头朝下,释放刷新箭头朝上)

我先把刷新头的布局代码贴上来,因为没有图的话实在不好讲
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/refresh_header_refresh"
android:layout_width="match_parent"
android:layout_height="80dp">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_margin="5dp">
<ProgressBar
android:id="@+id/refresh_header_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/refresh_header_arrow"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/common_listview_headview_red_arrow"/>
</FrameLayout>
<LinearLayout
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/refresh_header_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下拉刷新"
android:textSize="18sp"
android:singleLine="true"
android:textColor="#000000"/>
<TextView
android:id="@+id/refresh_header_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2014-12-2 10:23"
android:textSize="16sp"
android:textColor="@android:color/darker_gray"
android:layout_marginTop="3dp"/>
</LinearLayout>
</RelativeLayout>
<!--自定义部分,通过代码动态添加-->
接下就是实现动画的代码,这里我把动画抽取能,这样就可以实现下拉刷新图标的旋转效果了
private void initAnimation() {
//由下往上
down2UpAnimation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
down2UpAnimation.setDuration(300);
down2UpAnimation.setFillAfter(true);//将动画停在这里
up2DownAnimation = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
up2DownAnimation.setDuration(300);
up2DownAnimation.setFillAfter(true);//将动画停在这里
}
接下这个也是我们最为关心的部分了,那就处理用户的交互(touch),核心的思想就是重写onTouchEvent()方法:记录你手指点击下来的Y坐标,再判断Action_MOve的值(这是一个很常见的处理啦),因为这个用户的touch是需要记录一些下拉刷新的状态的,现在我们不妨先定义出三个状态出来,以便我们在处理和做判断的时候有用

private static final int STATE_PULLDOWN_REFRESH = 0;//下拉刷新
private static final int STATE_RELEASE_REFRESH  = 1;//释放刷新
private static final int STATE_REFRESHING       = 2;//正在刷新
private int mCurrentState = STATE_PULLDOWN_REFRESH;//默认状态设置为下拉刷新


接下就是对onTouchEvent()方法进行重写,这里也是下拉刷新的核心部分
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
downX = (int) (ev.getX() + 0.5f); //因为下拉刷新更多只关注Y坐标,这个X坐标也记录下吧
//些许后面会用到
downY = (int) (ev.getY() + 0.5f);
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) (ev.getX() + 0.5f);
int moveY = (int) (ev.getY() + 0.5f);
int diffX = moveX - downX;
diffY = moveY - downY;
//要是第一个view可见的话
//如果正在刷新的话,就不做响应
if (mCurrentState == STATE_REFRESHING) {
break;
}
//如果第一个view是可见的情况下,且headerView完全可见
if (mCustomHeaderView != null) {
//如果customHeaderView没有完全显示出来的话,就不去响应下拉刷新
//取出listView的左上角的点
int[] lliw = new int[2];
this.getLocationInWindow(lliw);
Log.d(TAG, "listView Y :" + lliw[1]);
//取出customHeaderViwe左上角的点
int[] hliw = new int[2];
mCustomHeaderView.getLocationInWindow(hliw);
Log.d(TAG, " customHeader Y" + hliw[1]);
//还记得我们的业务吗?我们的listView是添加了一个自定义的轮播图片的
//这个是在做处理刷新时候的一个优化,当你的轮播图片在你的listView的下方的时候,说明现在你的listView可能正在刷新,又或着listView被你拉下来了,他正在复原,那么此时的话,它要是在做复位动画,就不响应你的下拉刷新,让它先完成自己该走的功能
if (hliw[1] < lliw[1]) {
//不响应下拉刷新
return super.onTouchEvent(ev);
}
}
if (getFirstVisiblePosition() == 0) {
if (diffY > 0) {
//希望看到刷新的view
//改变头布局的PaddingTop
mCurrentPaddingTop = diffY - mRefreshHeight;
mHeaderLayout.setPadding(0, mCurrentPaddingTop, 0, 0);
//如果paddingTop是负数值,说明刷新部分还没完全显露出来,现在还是处于下拉刷新的位置
if (mCurrentPaddingTop < 0 &&
mCurrentPaddingTop != STATE_PULLDOWN_REFRESH) {
mCurrentState = STATE_PULLDOWN_REFRESH;
Log.d(TAG, "当前状态为 下拉状态");
refreshUI(); //这里将刷新UI进行了抽取,主要是实现 将textView的状态改变,还有进度条的显示与隐藏
}
if (mCurrentPaddingTop >= 0 && mCurrentState != STATE_REFRESHING) {
Log.d(TAG, "当前状态 为释放刷新");
mCurrentState = STATE_RELEASE_REFRESH;
refreshUI();
}
//消费掉
return true;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
downX = 0;  //清空数据
downY = 0;  //清空数据
if (diffY < 0) {
break;
}
diffY = 0;
//松开时的逻辑
//如果现在的状态正处于松开刷新的状态
if (mCurrentState == STATE_RELEASE_REFRESH) {
int start = mCurrentPaddingTop;
int end = 0;//显示刷新部分的头
//将进行缓慢的复原:属性动画
doHeaderAnimation(start, end);
//现在处于正在刷新
mCurrentState = STATE_REFRESHING;
//刷新ui
refreshUI();
if (mListener != null) {
mListener.onRefreshing();
}
}
//考虑现在的状态是否为 下来刷新
if (mCurrentState == STATE_PULLDOWN_REFRESH) {
int start = mCurrentPaddingTop;
int end = -mRefreshHeight; //隐藏刷部分的新头
doHeaderAnimation(start, end);
}
break;
default:
break;
}
return super.onTouchEvent(ev);
}
更新UI的模块,还记得我们上面将下拉刷新分成了三个模块吗?下拉刷新 释放刷新 正在刷新,那么每个过程结束后 它UI的显示都是不一样的,所以根据上面的三个状态来进行 相应的ui的变化
/**
* 更新UI
*/
private void refreshUI() {
switch (mCurrentState) {
case STATE_PULLDOWN_REFRESH:
//下拉刷新
//1.箭头显示,进度不显示
mIvArrow.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
//2.箭头由上往下:动画操作
mIvArrow.startAnimation(up2DownAnimation);
//3.文本变为下拉刷新
mTvState.setText("下拉刷新");
break;
case STATE_RELEASE_REFRESH:
//释放刷新
//1.箭头显示,进度不显示
mIvArrow.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
//2.箭头由下往上:动画操作
mIvArrow.startAnimation(down2UpAnimation);
//3.文本变为 松开刷新
mTvState.setText("松开刷新");
break;
case STATE_REFRESHING:
//正在刷新
//清空动画
mIvArrow.clearAnimation();
//1.箭头不显示,进度显示
mIvArrow.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
//2.文本变为 正在刷新
mTvState.setText("正在刷新");
break;
default:
break;
}
}
接下来就是做一个属性动画用作动画的过渡作用,要是没有属性动画的话 那么当你 释放刷新的时候 那个刷新的箭头就会 嗖的一个 飞上去了,这样的用户体验是很糟糕的,太突兀了. 那么再啰嗦一下
属性动画和补间原理:属性动画是先显示数据再去更新UI.  就好比说我们现在有0~99这个数,那么我现在要将99往0趋近, 补间动画呢 就是嗖的一下就到0了,而属性动画呢 因为属性动画奉行的原则是"先显示数据 再跟新UI"所以做了一个慢动作, 他就是这样的 99 98 97...这样一点一点了走,当然cpu的一点一点,就是我们毫秒级甚至 所以速度和用户体验都是可以接受的啦

private void doHeaderAnimation(int start, int end) {
//属性动画 是先显示数据 再更新ui
//模拟数据为 99->0   99-98-97...
ValueAnimator animator = ValueAnimator.ofInt(start, end);
animator.setDuration(300);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int animatorValue = (Integer) valueAnimator.getAnimatedValue();
mHeaderLayout.setPadding(0, animatorValue, 0, 0);
}
});
animator.start();
}
好了下拉刷新的基本上已经讲完了剩下的就是给出接口回调了,回调接口没什么好说的,就是一个萝卜一个坑,一套模板代码,反正你只用写好这个下拉刷新的框架,抛出接口给人家调用的哥们去实现里面的逻辑就好啦

//在自定义下拉刷新控件里头完成如下代码
//1.设置刷新的数据接口
public interface OnRefreshLintener(){
//正在刷新数据的回调
void onRefreshing();
}
//2.设置监听
public void setOnRefreshLintener(OnRefreshLintener() listener){
this.mListener = listener;
}
------------------------------------------------------------------
------------------------------------------------------------------
//3.调用接口,通知正在刷新
//这个一段代码呢要写MotionEvent.ACTION_UP里头,当你下拉释放完成后就经行刷新
if(mListener != null){
mListener.onRefreshing();
}
在自定义控件里头完成了接口的设置和监听,那个么调用者要怎么用呢?其实也很简单,下面几行代码就搞定了,这里头在啰嗦一句下拉刷新的业务怎么实现呢?很简单啦,无非就是通过网络重新再获取一遍数据就好啦
//4.使用监听,在controller中使用 //NewsController中实现
mListener.setOnRefreshListener(this);
//实现接口为实现的方法
onRefreshing(){
//正在刷新中
//重新请求当前页面的数据
}
好了,现在先开心一下下,我们的下拉刷新的部分的ui和业务逻辑实现都已经实现了,那么剩下的就是加载更多了.现在就讲讲加载更多的思想:在ui发面无非就是调用listView的addFooterView(),让后再根据setPadding(0,Top,0,0)的值进行加载更多UI的显示与隐藏,还是通过改变Top,还有一点需要强调的就是加载更多的操作由于是放在listView的最下方进行加载的,所以还要做一个onScrollListener()的监听,监听当滚动的状态发生改变的时候(通俗的讲就是,当你的listView滑动到最后一条的时候就把加载更多的那个自定义view显示出来)
private void initFooterLayout() {
//底部加载更多的布局
mFooterLayout = View.inflate(getContext(), R.layout.refresh_footer_layout, null);
//将其加载到listView的footer中去
this.addFooterView(mFooterLayout);
//通过setPadding,隐藏footer
mFooterLayout.measure(0, 0);
mFooterHeight = mFooterLayout.getMeasuredHeight();
mFooterLayout.setPadding(0, -mFooterHeight, 0, 0);
//设置listView的滚动监听
this.setOnScrollListener(this);  //当滚动到最后一条item的时候就把加载更多的view显示出来
}
实现listview的滚动监听未实现的方法

@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
int lastVisiblePosition = getLastVisiblePosition(); //获取最后一条可见的所在位置
int count = getAdapter().getCount();            //这个也就是listview的item的条目
if (lastVisiblePosition == count - 1 &&
(scrollState == OnScrollListener.SCROLL_STATE_FLING ||
scrollState == OnScrollListener.SCROLL_STATE_IDLE)) {
//有更多且不是加载更多
if (!isLoadMore && !noMore) {
isLoadMore = true;     //这里头做一些逻辑的判断
mFooterLayout.setPadding(0, 0, 0, 0);//完全显示最后一个
setSelection(count+2);
if (mListener != null) {
mListener.onLoadMore();     //设置监听器去加载更多的地方
}
}
}
}
@Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
}
还记得我们下拉刷新时做的数据接口吗,这里只要再加多一条加载更多的抽象方法就可以了

//1.设置刷新的的数据接口
public interface OnRefreshListener {
//正在刷新数据的回调
void onRefreshing();
//正在加载更多的时候回调
void onLoadMore();
}


3.下拉刷新和加载更多业务实现

最后还是啰嗦一句还是再啰嗦一句吧,怎么使用这个下拉刷新和加载更多呢,也就是怎么去实现暴露出来的回调接口,这个就相当简单了,由于这个listView和加载更多是在一个实际的项目中用到了,由于自己项目的需要所以做了一些优化和改进.

//设置刷新监听


mListView.setOnRefreshListener(this);//没错你没看错,就是这么简单,接着就只需要实现接口两个未
//实现的方法就可以了


接下就是实现onLoadMore(),onRefreshing()方法,这里只给出了核心的方法,一个萝卜一个坑,往里面填写数据就好了
//onRefreshing()无非就是重新去加载一遍网络
@Override
public void onRefreshing() {
final String url = Constans.BASE_URL + mUrl;
getDataFromNet(url,true);  //再这建议把加载网络的方法抽取出来
}
//加载更多
@Override
public void onLoadMore() {
if (TextUtils.isEmpty(mMoreUrl)) {
Toast.makeText(mContext, "没有更多数据可以加载", Toast.LENGTH_SHORT).show();
//设置加载完成没有更多的数据
mListView.setRefreshFinish(true);
return;
}
HttpUtils utils = new HttpUtils();
String url = Constans.BASE_URL + mMoreUrl;
utils.send(HttpRequest.HttpMethod.GET, url, new RequestCallBack<String>() {
@Override
public void onSuccess(ResponseInfo<String> responseInfo) {

}
@Override
public void onFailure(HttpException error, String msg) {

}
});
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息