俗话说好记性不如烂笔头,决定以后将研究过的东西写到博客里,方便自己以后查找,也方便技术分享。第一篇从基础的自定义下拉刷新开始。这里说下,我是在大神的肩膀上进行自定义的,因为自己重写下拉刷新的话会有很多边界,状态和动画等问题要处理,以前写过一次,效果和功能可以实现,但是有不少bug,而且封装也不好。因此直接使用android-Ultra-Pull-To-Refresh
效果图
本文是基于github上的一个开源项目:android-Ultra-Pull-To-Refresh(下面简称UltraPtr) ,有兴趣的同学可以去看看。废话不多说,开车。
1.准备headview头部布局
<?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:layout_centerHorizontal="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="0dp">
<ImageView
android:id="@+id/iv_ptr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="100dp"
android:src="@drawable/ptr_dra" />
<TextView
android:id="@+id/tv_ptr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下拉刷新..." />
</LinearLayout>
2. 实现PtrUIHandler,处理下拉刷新回调
这一步是最重要的,几乎所有的刷新样式变化的逻辑都是在这里进行处理的,看代码。
public class RefreshHeadView extends FrameLayout implements PtrUIHandler {
private Context context;
private ImageView iv;
private TextView tv;
private AnimationDrawable animationDrawable;
public RefreshHeadView(Context context) {
super(context);
this.context = context;
initView();
}
private void initView() {
View.inflate(context, R.layout.ptrheadview, this);
iv = (ImageView) findViewById(R.id.iv_ptr);
tv = (TextView) findViewById(R.id.tv_ptr);
animationDrawable = (AnimationDrawable) iv.getDrawable();
}
public RefreshHeadView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RefreshHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void onUIReset(PtrFrameLayout frame) {
//onUIRefreshComplete之后调用,用于复位
setImageAndText(R.mipmap.ptr_loading_9, "刷新完成...");
}
@Override
public void onUIRefreshPrepare(PtrFrameLayout frame) {
//开始下拉的时候调用一次
setImageAndText(R.mipmap.ptr_loading_1, "下拉刷新...");
}
@Override
public void onUIRefreshBegin(PtrFrameLayout frame) {
//正在刷新的时候调用,开始帧动画
iv.setImageDrawable(animationDrawable);
animationDrawable.start();
tv.setText("F5的能量女朋友出现了");
}
@Override
public void onUIRefreshComplete(PtrFrameLayout frame) {
//刷新完成的时候调用
animationDrawable.stop();
setImageAndText(R.mipmap.ptr_loading_9, "刷新完成...");
}
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
//触发刷新的高度 默认是头部高度的1.2倍
final int offsetToRefresh = ptrIndicator.getOffsetToRefresh();
//当前下拉的高度
final int currentPos = ptrIndicator.getCurrentPosY();
//计算下拉的百分比
int persent = (int) (((double) currentPos / offsetToRefresh) * 100);
//只有在prepare状态的时候才执行下面的替换图片
if (status!=PtrFrameLayout.PTR_STATUS_PREPARE) {
return;
}
//根据不同的比例替换不同的图片和文字
if (persent < 50) {
setImageAndText(R.mipmap.ptr_loading_1, "下拉刷新");
} else if (persent < 60) {
setImageAndText(R.mipmap.ptr_loading_2, "下拉刷新");
} else if (persent < 70) {
setImageAndText(R.mipmap.ptr_loading_3, "下拉刷新");
} else if (persent < 80) {
setImageAndText(R.mipmap.ptr_loading_4, "下拉刷新");
} else if (persent < 100) {
setImageAndText(R.mipmap.ptr_loading_5, "下拉刷新");
} else {
setImageAndText(R.mipmap.ptr_loading_6, "松开松开~");
}
}
private void setImageAndText(int res, String text) {
iv.setImageResource(res);
tv.setText(text);
}
}
代码里基本上注释都写很清楚了,只需要注意几个地方。
在构造方法中initView().将刚才定义的headview填充进来,并找到里面的imageview和textview
实现PtrUIHandler,需要重写几个方法。
onUIRefreshPrepare 下拉准备,刚开始下拉的时候就会调用一次。这里我也没什么好准备的,就设置了第一张图片。
onUIRefreshBegin 这是刷新的时候调用一次,按照acfun的下拉刷新,会显示一个萌萌的妹纸动画。所以对图片设置帧动画,并开启帧动画,这样刷新的时候就是小人动的效果
onUIRefreshComplete 刷新完成,停止帧动画
onUIRefreshReset 复位,最后调用,设置了一张图片。
onUIPositionChange 这是比较复杂一些的方法。这个回调会传给我们下拉的距离和手指是否在下拉等参数,用于下拉的时候变换图片。这里介绍一下参数,frame不用说了,父类。isUnderTouch,表示手指是否按在屏幕上。status:表示当前的刷新状态,有init prepare loading comlete四种。最后ptrIndicator是手势类。首先获取到触发刷新的高度,这个是可以自己设置的,默认头部高度1.2倍。然后获取当前下拉的高度,根据两个高度计算下拉的百分比。最后根据百分比来替换不同的图片就可以了。其中有一点要注意,只有status在prepare的时候才执行替换图片,否则高度每次变化都会调用此方法不停替换图片。
//只有在prepare状态的时候才执行下面的替换图片
if (status!=PtrFrameLayout.PTR_STATUS_PREPARE) {
return;
}
至此,整个headview和uihandler都处理完成。
3,自定义PullToRefreshLayout
public class PullToRefreshLayout extends PtrFrameLayout {
private Context context;
private float startY;
private float startX;
// 记录viewPager是否拖拽的标记
private boolean mIsHorizontalMove;
// 记录事件是否已被分发
private boolean isDeal;
// viewpager触发滑动的距离
private int mTouchSlop;
private ArrayList<ViewPager> mViewPagers = new ArrayList<>();
public PullToRefreshLayout(Context context) {
super(context,null);
}
public PullToRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.context=context;
initView();
}
private void initView() {
RefreshHeadView headview = new RefreshHeadView(context);
//设置headview
setHeaderView(headview);
//设置uihandler回调 因为headview实现了PtrUIHandler接口,所以这里还是headview
addPtrUIHandler(headview);
//阻尼系数 默认1.7f 越大下拉越吃力
setResistance(1.7f);
//触发刷新时移动的位置比例 默认,1.2f,移动达到头部高度1.2倍时可触发刷新操作。
setRatioOfHeaderHeightToRefresh(1.2f);
//回弹延时 默认 200ms,回弹到刷新高度所用时间
setDurationToClose(500);
//头部回弹时间 默认1000ms
setDurationToCloseHeader(1500);
//下拉刷新还是释放刷新 默认下拉刷新default is false
setPullToRefresh(false);
// default is true 刷新时是否保持头部
setKeepHeaderWhenRefresh(true);
//viewpager处理
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
}
//处理下拉刷新和viewpager滑动冲突
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mViewPagers.size() ==0) {
return super.dispatchTouchEvent(ev);
}
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 记录手指按下的位置
startY = ev.getY();
startX = ev.getX();
// 初始化标记
mIsHorizontalMove = false;
isDeal = false;
break;
case MotionEvent.ACTION_MOVE:
// 如果已经判断出是否由横向还是纵向处理,则跳出
if (isDeal) {
break;
}
/**拦截禁止交给Ptr的 dispatchTouchEvent处理**/
mIsHorizontalMove = true;
// 获取当前手指位置
float endY = ev.getY();
float endX = ev.getX();
float distanceX = Math.abs(endX - startX);
float distanceY = Math.abs(endY - startY);
if (distanceX != distanceY) {
// 如果X轴位移大于Y轴位移,那么将事件交给viewPager处理。
//横向滑动的距离大于触发viewpager滑动事件的距离并且 横向大于竖向
if (distanceX > mTouchSlop && distanceX > distanceY) {
mIsHorizontalMove = true;
isDeal = true;
} else if (distanceY > mTouchSlop) {
mIsHorizontalMove = false;
isDeal = true;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//下拉刷新状态时如果滚动了viewpager 此时mIsHorizontalMove为true 会导致PtrFrameLayout无法恢复原位
// 初始化标记,
mIsHorizontalMove = false;
isDeal = false;
break;
}
if (mIsHorizontalMove) {
//相当于拦截事件,不交给ptr处理,直接向下分发
return dispatchTouchEventSupper(ev);
}
//交给ptr,不做拦截,正常下拉
return super.dispatchTouchEvent(ev);
}
@Override
protected void onLayout(boolean flag, int i, int j, int k, int l) {
super.onLayout(flag, i, j, k, l);
if (flag) {
getAlLViewPager(mViewPagers, this);
}
}
/**
* 获取SwipeBackLayout里面的ViewPager的集合
*/
private void getAlLViewPager(List<ViewPager> mViewPagers, ViewGroup parent) {
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
if (child instanceof ViewPager) {
mViewPagers.add((ViewPager) child);
} else if (child instanceof ViewGroup) {
getAlLViewPager(mViewPagers, (ViewGroup) child);
}
}
}
}
这个类就是我们最终要使用到布局里面的类。比较简单,分为两个部分
1.initPara() 用于设置各种参数,具体看代码都有注释
2,处理下拉刷新和viewpager的滑动冲突。虽然UIPtr的作者也更新了处理冲突的方法,但是处理的并不太好,在viewpager上的滑动还是太敏感。所以这里重写dispatchTouchEvent方法,判断子view里是否有viewpager,如果没有,直接交给父类处理。如果有,判断滑动方向和距离。如果是横向滑动,那么就不要交给父类处理,直接跳过父类,交给父类的父类处理。否则就是正常下拉刷新,还是交给父类。
至此,整个自定义下拉刷新完成。
源码地址 https://github.com/itwangyu/MyPullRefreshDemo