如果想学习更多进阶知识,可以关注我的微信公众号:Android小菜。
也可以直接扫描二维码关注:
转载本专栏文章,请注明出处,尊重原创 。文章博客地址:道龙的博客
本篇是实现仿美团下拉刷新控件的第二篇,第一篇见:仿美团下拉刷新控件(一)
文本最终实现效果如下:
由于之前写过三篇自定义下拉刷新控件的文章,而且当时写的非常非常的细,本篇就不再浪费大家时间了,直接把重要的地方做一些解释,感兴趣的朋友可以直接下载源码。
1、创建一个自定义View类RefreshListView继承自ListView。
此时这个RefreshListView可以当做一个ListView来使用了。
2、在构造方法中初始化头布局
/**
* 初始化头
*/
private void initHearder() {
mHeaderView = LayoutInflater.from(mContext).inflate(R.layout.layout_header,null);
mFirstStepView = (RefreshFirstStepView) mHeaderView.findViewById(R.id.first_view);
mSecondStepView = (RefreshSecondStepView) mHeaderView.findViewById(R.id.second_view);
mThirdStepView = (RefreshThirdStepView) mHeaderView.findViewById(R.id.third_view);
mTvDes = (TextView) mHeaderView.findViewById(R.id.tv_pull_to_refresh);
mHeaderView.measure(0,0);
mHeaderHeight = mHeaderView.getMeasuredHeight();
// Log.e(TAG,"头高度为:--->"+mHeaderHeight);
mHeaderView.setPadding(0,-mHeaderHeight,0,0);
addHeaderView(mHeaderView);
}
看到这里初始化了一个布局,布局样式:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
>
<com.itydl.a05.view.RefreshFirstStepView
android:id="@+id/first_view"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"
/>
<com.itydl.a05.view.RefreshSecondStepView
android:id="@+id/second_view"
android:layout_width="45dp"
android:visibility="gone"
android:layout_height="45dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"
/>
<com.itydl.a05.view.RefreshThirdStepView
android:id="@+id/third_view"
android:layout_width="45dp"
android:layout_height="45dp"
android:visibility="gone"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"
/>
<TextView
android:id="@+id/tv_pull_to_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="下拉刷新"
/>
</LinearLayout>
实现下拉刷新,肯定是要加一个头布局的嘛,这个头布局就作为下拉刷新的布局。通过打气筒拿到对应的View实例,然后拿到它的孩子控件,它里面有三个孩子控件,这三个孩子控件都是通过自定义View的方式引入的,具体的实现可见仿美团下拉刷新控件(一)
从上往下一次代表:下拉刷新状态展示()的布局,释放刷新状态展示的布局,正在刷新状态展示的布局。最底部的TextView就代表当前状态的文案。往下调用头布局的测量方法拿到头布局的高度,设置padding值后,就默认状态下把头布局隐藏起来了。然后通过addHeaderView()的方式把头布局添加到自定义的ListView当中:
3、重写onTouchEvent事件,处理交互:
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if(downY == -1){
downY = ev.getY();
}
float moveY = ev.getY();
//如果当前处于下拉刷新状态,不去走下面的代码
if(currentSatae == REFRESHING){
return false;//如果当前处于正在刷新状态,取消后续的所有事件包括上滑和下滑
}
float instance = (moveY - downY)/3;
if(getFirstVisiblePosition() == 0 && instance >= 0){//0item且下拉
int topPadding = (int) (-mHeaderHeight + instance +0.5f);//计算padding值
if(topPadding > -mHeaderHeight){//只有padding值大于-mHeaderHeight的时候才去自己处理事件
if(topPadding < 0 && currentSatae != PULL_TO_REFRESH){
// Log.e(TAG,"下拉刷新状态");
currentSatae = PULL_TO_REFRESH;
}else if(topPadding >= 0 && currentSatae != RELEASE_TO_REFRESH){
// Log.e(TAG,"释放刷新状态");
currentSatae = RELEASE_TO_REFRESH;
}
}
mHeaderView.setPadding(0,topPadding,0,0);
refreshState(instance);
return true;//自己处理事件
}
break;
case MotionEvent.ACTION_UP:
downY = -1;
if(currentSatae == RELEASE_TO_REFRESH){
//当前状态是释放刷新,松手后是正在刷新状态
currentSatae = REFRESHING;
//还原Padding,使用Scroll平滑移动过去
mHeaderView.setPadding(0,0,0,0);
refreshState(mHeaderHeight);
if(mOnRefreshListener != null){
mOnRefreshListener.onRefreshing();
}
}else if(currentSatae == PULL_TO_REFRESH){
//使用Scroll平滑移动过去
mHeaderView.setPadding(0,-mHeaderHeight,0,0);
}
break;
default:
break;
}
return super.onTouchEvent(ev);//ListView的事件保留
}
对这段代码解释一下:
在按下的时候记录一个坐标,downY这个值初始化值为-1,当move的时候如果这个值为-1,说明down事件没有获取到当前的坐标Y值(是有可能发生的),所以在move的时候先判断downY是否拿到没有拿到这个值就在move的时候记录第一个值。然后往下,判断当前处于什么状态,如果是下拉刷新状态,就不走任何逻辑了返回了false表示后续的事件都拿不到。然后MOVE事件里面拿到移动的距离,只有是手指move位置相对初识位置downY插值是大于零的表示头布局往下拉了一些并且当前处于第0个Item的时候才去走自己的逻辑处理下拉刷新状态。从而就确定了当前状态。随着移动的距离计算,如果超过了头布局的高度时候属于释放刷新状态,少于头布局的高度为下拉刷新状态。然后记录下当前状态,并且通过设置padding值改变头布局的位置,最后调用了refreshState(instance);方法来根据当前的状态值刷新控件的样式以及动画的加载等,把(moveY-downY)/3这个值传过去,因为待会要用到它计算百分比。(moveY-downY)/3的另一个原因是不让下拉头布局过多的距离。
然后是UP事件,如果UP的时候处于下拉刷新状态,那么就恢复原来的状态,这里使用很暴力的方式直接设置padding了。如果当前是释放刷新状态松手应该是进入正在刷新状态了,正在刷新状态的时候就要调用接口方法了,在这个方法里面进行具体的耗时操作,比如访问网络最新数据等。然后在调用refreshState(instance);方法来根据当前的状态值刷新控件的样式以及动画的加载。
看一下refreshState(instance);这个方法:
/**
* 刷新当前的状态
* 根据标记为处理控件的样式数据
* @param instance
*/
private void refreshState(float instance) {
//计算比例
float moveY = Math.abs(instance);
float percent = moveY / mHeaderHeight;
if(percent >= 1.0f){
percent = 1.0f;
}
Log.e(TAG,"百分比-->"+percent);
switch (currentSatae) {
case PULL_TO_REFRESH:
mFirstStepView.setVisibility(VISIBLE);
mSecondStepView.setVisibility(GONE);
mThirdStepView.setVisibility(GONE);
mTvDes.setText("下拉刷新");
//执行动画
mFirstStepView.setProgress(percent);
mFirstStepView.postInvalidate();
break;
case RELEASE_TO_REFRESH:
mFirstStepView.setVisibility(GONE);
mSecondStepView.setVisibility(VISIBLE);
mThirdStepView.setVisibility(GONE);
mTvDes.setText("释放刷新");
secondAnim.start();
break;
case REFRESHING:
mFirstStepView.setVisibility(GONE);
mSecondStepView.setVisibility(GONE);
mThirdStepView.setVisibility(VISIBLE);
mTvDes.setText("正在刷新");
mThreeAnim.start();
break;
default:
break;
}
}
通过判断不同的状态来做对应的控件处理,无非就是某个状态展示对应状态的动画,以及文案的修改。这里讲一下最开始计算百分数:
//计算比例
float moveY = Math.abs(instance);
float percent = moveY / mHeaderHeight;
if(percent >= 1.0f){
percent = 1.0f;
}
手指移动距离/头的高度-->0.0f-->1.0f,比如头高度是100,移动了20,那么percent=0.2f以此类推。但是在下拉的时候moveY可能会超过头的高度,如果超过了,就按照1.0f处理。4、使用自定义下拉刷新控件
在Activity或者Fragment中:
public class TestActivity extends AppCompatActivity implements RefreshListView.OnRefreshListener {
private static final int REFRESH_COMPLETE = 0;
private RefreshListView mListView;
private ArrayList<String> mDatas;
/**
* MyHandler是一个私有静态内部类继承自Handler,内部持有MainActivity的弱引用,
* 避免内存泄露
*/
private MyHandler mHandler = new MyHandler(this);
private ArrayAdapter<String> mAdapter;
private static class MyHandler extends Handler{
private WeakReference<TestActivity> mActivity;
public MyHandler(TestActivity activity){
mActivity = new WeakReference<TestActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
TestActivity activity = mActivity.get();
if (activity != null) {
switch (msg.what) {
case REFRESH_COMPLETE:
activity.mListView.setOnRefreshComplete();
activity.mAdapter.notifyDataSetChanged();
activity.mListView.setSelection(0);
break;
}
}else{
super.handleMessage(msg);
}
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mListView = (RefreshListView) findViewById(R.id.lv_refresh);
mDatas = new ArrayList<String>(Arrays.asList(Strings.NAMES));
mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, mDatas);
mListView.setAdapter(mAdapter);
mListView.setOnRefreshListener(this);
}
@Override
public void onRefreshing() {
//开启子线程访问网络
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(3000);
mDatas.add(0, "new data");
mHandler.sendEmptyMessage(REFRESH_COMPLETE);
}
}).start();
}
}
直接在刷新的方法中进行耗时操作,进行访问网络的功能,以及成功之后添加数据功能,然后使用Handler发送消息,在处理消息的方法中进行数据的刷新。值得注意的是,这里为了防止内存泄漏做了一点点操作,相信都能明白是什么意思,因为之前文章也都说到过如何避免内存泄漏问题。
然后运行程序:
这样这个控件就讲完了,如果觉得对你有帮助记得加下关注哈,或者微信关注公众号——Android小菜。