目录
- 1、创建刷新控件
- 1.1、创建头部View
- 1.2、下拉事件
- 1.3、接口回调
- 2、上拉加载更多
- 2.1、底部样式
- 2.2、布局添加
Listview现在用的很少了,基本都是使用Recycleview,但是不得不说Listview具有划时代的意义,拓展性很强,我们可以自己添加下拉刷新,上拉加载更多功能。他和recycleview不同,他生来具有addHeaderView和addFooterView的功能,这也导致同样都是列表控件,实现上拉下拉的方式缺截然不同。
效果图
1、创建刷新控件
public class MyListview extends ListView { | |
public MyListview(Context context) { | |
this(context,null); | |
} | |
public MyListview(Context context, AttributeSet attrs) { | |
this(context,attrs,0); | |
} | |
public MyListview(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(context); | |
} | |
public void init(Context context) { | |
} |
1.1、创建头部View
头部样式,我写的是最简单的,根据业务需求来定,下拉的时候无非就是几种
- 产品logo作为箭头转动的icon
- 添加刷新时间
- 加入其他具有特色的动效
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:orientation="horizontal" | |
android:gravity="center" | |
tools:ignore="MissingDefaultResource"> | |
<ProgressBar | |
android:layout_width="20dp" | |
android:layout_height="match_parent" | |
style="?android:attr/progressBarStyle" | |
android:layout_marginEnd="10dp" | |
android:indeterminateTint="#E8AD56" | |
/> | |
<TextView | |
android:id="@+id/header_text" | |
android:layout_width="wrap_content" | |
android:layout_height="50dp" | |
android:text="我是头部" | |
android:gravity="center" | |
android:textColor="#E8AD56"/> | |
</LinearLayout> |
样式写好后,我们需要添加到我们控件中,addHeaderView就起到作用了
public void init(Context context) { | |
//添加头部 | |
viewHeader = View.inflate(context, R.layout.view_header, null); | |
viewHeader.measure(0, 0); | |
//让系统自动检测头部高度 | |
heightHeader = viewHeader.getMeasuredHeight(); | |
header_text = viewHeader.findViewById(R.id.header_text); | |
viewHeader.setPadding(0, -heightHeader, 0, 0); | |
this.addHeaderView(viewHeader); | |
} |
一些控件我定义成全局的是因为下面会用到。setPadding可以设置显示位置,左上右下,上为负数就是隐藏在顶部。我们需要手指下拉去控制他缓慢显示,就用到了OnTouchListener,我们实现OnTouch方法做一些事件的分发处理。
1.2、下拉事件
public class MyListview extends ListView implements View.OnTouchListener{ | |
public boolean onTouch(View v, MotionEvent event) { | |
return false; | |
} | |
public void init(Context context){ | |
setOnTouchListener(this); | |
} | |
} |
注意:别忘记了setOnTouchListener在init中添加
这时候我们需要对event的down,move,up事件进行逻辑处理,当手指按在屏幕时会触发事件,一个down事件,0~无数次move事件,一个up事件,这里面着重对move事件做处理,我们记录一下down事件的Y,因为是上下拉动,没必要计算X。然后diffY就是手指滑动的距离,我们需要处理一下这个值,因为值太大,而且值是整数,会让我们下拉的时候产生错乱,我们本意是让其从-100到0缓慢滑出(比如头部高度是100,从隐藏到显示就是-100到0),小伙伴都可以试试viewHeader.setPadding(0, diffY, 0, 0);和viewHeader.setPadding(0, paddY, 0, 0);效果是不一样的,diff/3是让其有种阻尼的感觉,不然的话会很块就被拉出来了。还有事件消费的话一定要return true。
@Override | |
public boolean onTouch(View v, MotionEvent event) { | |
switch (event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
downY = (int) event.getY(); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
moveY = (int) event.getY(); | |
diffY = moveY - downY; | |
//滑动的距离 | |
paddY = - heightHeader + diffY / 3; | |
viewHeader.setPadding(0, paddY, 0, 0); | |
return true; | |
//break; | |
case MotionEvent.ACTION_UP: | |
break; | |
} | |
return false; | |
} |
我们下面要做下拉时给人的反馈,我们下拉时有3种状态分别是
- 开始刷新 (下拉距离不超过100)
- 释放刷新 (下拉距离超过100)
- 刷新中 (手指释放,up事件处理)
//属性-开始刷新状态 | |
private final int PULL_REFRESH_STATE = 0; | |
private final int PULL_REFRESH_RELEASE = 1;//释放刷新 | |
private final int PULL_REFRESHING = 2;//正在刷新 | |
private int pull_current_state = PULL_REFRESH_STATE;//当前状态 | |
public void updateHeaderState() { | |
switch (pull_current_state) { | |
//开始 | |
case PULL_REFRESH_STATE: | |
header_text.setText("开始刷新"); | |
viewHeader.setPadding(0, -heightHeader, 0, 0); | |
break; | |
//释放 | |
case PULL_REFRESH_RELEASE: | |
header_text.setText("释放刷新"); | |
break; | |
//正在 | |
case PULL_REFRESHING: | |
header_text.setText("刷新中......."); | |
viewHeader.setPadding(0, 0, 0, 0); | |
break; | |
} | |
} |
我们只需要判断是否处于刷新中,如果不是,则计算是不是第一个item可见并且滑动距离大于0,证明手指滑动了。然后不同滑出高度显示不同的文字即可。
@Override | |
public boolean onTouch(View v, MotionEvent event) { | |
switch (event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
downY = (int) event.getY(); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
moveY = (int) event.getY(); | |
diffY = moveY - downY; | |
//滑动的距离 | |
paddY = - heightHeader + diffY / 3; | |
//如果是正在刷新中不做任何操作 | |
if (pull_current_state == PULL_REFRESHING) { | |
return false; | |
} | |
// 下拉//第一条,并且滑动距离大于0 | |
if (getFirstVisiblePosition() == 0 && diffY > 0) { | |
if (paddY > 0 && pull_current_state != PULL_REFRESH_RELEASE) { | |
//释放刷新 | |
pull_current_state = PULL_REFRESH_RELEASE; | |
updateHeaderState(); | |
} else if (paddY < 0 && pull_current_state != PULL_REFRESH_STATE) { | |
pull_current_state = PULL_REFRESH_STATE; | |
header_text.setText("开始刷新"); | |
updateHeaderState(); | |
} | |
viewHeader.setPadding(0, paddY, 0, 0); | |
return true; | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
if (pull_current_state == PULL_REFRESH_STATE) { | |
updateHeaderState(); | |
} else if (pull_current_state == PULL_REFRESH_RELEASE) { | |
pull_current_state = PULL_REFRESHING; | |
updateHeaderState(); | |
} | |
break; | |
} | |
return false; | |
} |
1.3、接口回调
我们需要状态根据业务来动态调整,在可以刷新的时候做一些逻辑处理,同时处理完了,调整状态。
public void setPullDownFinish() { | |
pull_current_state = PULL_REFRESH_STATE; | |
viewHeader.setPadding(0, -heightHeader, 0, 0); | |
} | |
public IPullDownRefreshService iPullDownRefreshService; | |
public interface IPullDownRefreshService { | |
void onPullDownRefresh();//下拉刷新 | |
void onLoadMore();//上拉加载更多刷新 | |
} | |
public void setOnRefreshListener(IPullDownRefreshService iPullDownRefreshService) { | |
this.iPullDownRefreshService = iPullDownRefreshService; | |
} |
准备工作做好后,我们在更新状态的地方调用
public void updateHeaderState() { | |
switch (pull_current_state) { | |
//开始 | |
case PULL_REFRESH_STATE: | |
break; | |
//释放 | |
case PULL_REFRESH_RELEASE: | |
break; | |
//正在 | |
case PULL_REFRESHING: | |
header_text.setText("刷新中......."); | |
viewHeader.setPadding(0, 0, 0, 0); | |
if (iPullDownRefreshService != null) { | |
iPullDownRefreshService.onPullDownRefresh(); | |
} | |
break; | |
} | |
} |
Activity中使用,来一个3秒刷新完成
mBinding.listview.setOnRefreshListener(new MyListview.IPullDownRefreshService() { | |
public void onPullDownRefresh() { | |
refreshSuccess(); | |
Toast.makeText(ActivityRefresh.this, "下拉-加载中.....", Toast.LENGTH_SHORT).show(); | |
} | |
public void onLoadMore() { | |
Toast.makeText(ActivityRefresh.this, "more-加载中.....", Toast.LENGTH_SHORT).show(); | |
} | |
}); | |
CountDownTimer countDownTimer; | |
public void refreshSuccess() { | |
if (countDownTimer == null) { | |
countDownTimer = new CountDownTimer(3000, 1000) { | |
public void onTick(long millisUntilFinished) { | |
} | |
public void onFinish() { | |
mBinding.listview.setPullDownFinish(); | |
countDownTimer.cancel(); | |
countDownTimer = null; | |
} | |
}.start(); | |
} | |
} |
到这下拉刷新就结束了。
上拉加载更多也是如此
2、上拉加载更多
2.1、底部样式
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:orientation="horizontal" | |
android:gravity="center" | |
tools:ignore="MissingDefaultResource"> | |
<ProgressBar | |
android:id="@+id/footer_prggress" | |
android:layout_width="20dp" | |
android:layout_height="match_parent" | |
style="?android:attr/progressBarStyle" | |
android:layout_marginEnd="10dp" | |
android:indeterminateTint="#E8AD56" | |
/> | |
<TextView | |
android:id="@+id/footer_text" | |
android:layout_width="wrap_content" | |
android:layout_height="70dp" | |
android:text="正在加载更多" | |
android:gravity="center" | |
android:textColor="#E8AD56"/> | |
</LinearLayout> |
2.2、布局添加
public void init(Context context) { | |
viewFooter = View.inflate(context, R.layout.view_footer, null); | |
viewFooter.measure(0, 0); | |
footer_text = viewFooter.findViewById(R.id.footer_text); | |
footer_prggress = viewFooter.findViewById(R.id.footer_prggress); | |
heightFooter = viewFooter.getMeasuredHeight(); | |
viewFooter.setPadding(0, -heightFooter, 0, 0); | |
this.addFooterView(viewFooter); | |
} |
到这里也布局算是添加完毕了,加载更多实现方式不同,我罗列两种
- 结合OnTouchListener
- 结合OnScrollListener
方式一
这里我先说方式一,因为我们下拉也是用的OnTouchListener,上拉加载也有几种状态,有加载中,还有暂无数据,普遍大家会写, 已经到底了~,一直显示在最底部,提示用户没数据了,从而我们控件的上拉事件不可触发状态。
private final int MORE_LOAD_STATE = 10; | |
private final int MORE_LOADING = 11; | |
private final int MORE_NO = 12;//已加载全部数据 | |
private int more_current_state = MORE_LOAD_STATE;//当前状态 |
只有一点需要注意paddFooterY = paddFooterY > heightFooter ? 0 : paddFooterY;来判断不能滑出底部的高度。
@Override | |
public boolean onTouch(View v, MotionEvent event) { | |
switch (event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
downY = (int) event.getY(); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
moveY = (int) event.getY(); | |
diffY = moveY - downY; | |
//滑动的距离 | |
paddFooterY = heightFooter - diffY/3 ; | |
//上拉加载更多 | |
if (getLastVisiblePosition() == getCount() - 1 && more_current_state != MORE_NO) { | |
if( more_current_state!=MORE_LOADING){ | |
more_current_state = MORE_LOADING; | |
if (iPullDownRefreshService != null) { | |
iPullDownRefreshService.onLoadMore(); | |
} | |
} | |
paddFooterY = paddFooterY > heightFooter ? 0 : paddFooterY; | |
viewFooter.setPadding(0,paddFooterY , 0, 0); | |
return true; | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
break; | |
} | |
return false; | |
} |
方式二
public class MyListview extends ListView implements View.OnTouchListener, AbsListView.OnScrollListener{ | |
public void onScrollStateChanged(AbsListView view, int scrollState) { | |
//没有滚动,或者已经用户触摸滚动动画结束 | |
if(scrollState == SCROLL_STATE_IDLE || scrollState == SCROLL_STATE_FLING){ | |
//最后一个可见 | |
if(getLastVisiblePosition() == getCount()-1){ | |
viewFooter.setPadding(0,0,0,0); | |
if(iPullDownRefreshService!=null){ | |
iPullDownRefreshService.onLoadMore(); | |
} | |
} | |
} | |
} | |
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { | |
} | |
} |
注意:在init方法中添加
public void init(Context context) { | |
setOnTouchListener(this); | |
...... | |
this.setOnScrollListener(this); | |
} |