SwipeToLoadLayout是一个可重用的下拉刷新和上拉加载控件,理论上支持各种View和ViewGroup(ListView,ScrollView,RecyclerView,GridView,WebView,Linearlayout,RelativeLayout,FrameLayout,ImageView,TextView等等)的刷新和加载,还支持自动刷新,手动刷新,自动加载,手动加载,禁止刷新,禁止加载等操作,同时也可以自定义头部和尾部,头部还分classic,above,blow,scale四种类型,还有自动刷新的效果,体验也很流畅。
这里我们使用它来实现我们所常见的几种下拉刷新效果,如百度外卖、饿了吗、京东商城、美团外卖、天猫、微博及天气的下拉刷新效果;
第一步:首先在build.gradle(Project:项目名称)中的repositories下面添加JitPack代码库,如下:
repositories {
......
maven { url "https://jitpack.io" }
}
第二步:在build.gradle(Module:app)中添加依赖项,如下:
dependencies {
compile 'com.github.Aspsine:SwipeToLoadLayout:1.0.4'
}
第三步:接下来自定义头部刷新控件,这里笔者先自定义了一个饿了吗的下拉刷新控件,自定义头部刷新控件需要继承自SwipeRefreshHeaderLayout,代码如下:
public class ELeMaRefreshHeaderView extends SwipeRefreshHeaderLayout {
private int mHeaderHeight;
private boolean rotated = false;
// 创建Handler发送延迟执行的指令
private Handler mHandler = new Handler();
private ImageView imageViewDrumstick;
private ImageView imageViewHotPot;
private ImageView imageViewRice;
private ImageView imageViewVegetable;
private RelativeLayout imageViewPotCover;
public ELeMaRefreshHeaderView(Context context) {
this(context, null);
}
public ELeMaRefreshHeaderView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ELeMaRefreshHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHeaderHeight = getResources().getDimensionPixelOffset(R.dimen.refresh_header_height_cook);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
imageViewDrumstick = (ImageView) findViewById(R.id.image_view_drumstick);
imageViewHotPot = (ImageView) findViewById(R.id.image_view_hot_pot);
imageViewRice = (ImageView) findViewById(R.id.image_view_rice);
imageViewVegetable = (ImageView) findViewById(R.id.image_view_vegetable);
imageViewPotCover = (RelativeLayout) findViewById(R.id.image_view_pot_cover);
}
@Override
public void onRefresh() {
//正在刷新,执行动画
imageViewPotCover.setVisibility(View.INVISIBLE);
imageViewDrumstick.setVisibility(View.VISIBLE);
imageViewHotPot.setVisibility(View.VISIBLE);
imageViewRice.setVisibility(View.VISIBLE);
imageViewVegetable.setVisibility(View.VISIBLE);
imageViewPotCover.setAlpha(0);
startAnimation();
}
@Override
public void onPrepare() {
}
@Override
public void onMove(int y, boolean isComplete, boolean automatic) {
if (!isComplete) {
if (y >= mHeaderHeight) {
if (!rotated) {
rotated = true;
}
} else if (y < mHeaderHeight) {
float tan = (float) (y - mHeaderHeight * 2 / 3) / (float) (imageViewPotCover.getWidth());
int angle = (int) (tan * 90);
if (angle > 30) {
angle = 30;
}
if (angle < 0) {
angle = 0;
}
imageViewPotCover.setRotation(-angle);
}
}
}
@Override
public void onRelease() {
Log.d("CookRefreshHeaderView", "onRelease()");
}
@Override
public void onComplete() {
rotated = false;
mHandler.removeCallbacksAndMessages(null);
imageViewPotCover.setVisibility(View.VISIBLE);
imageViewDrumstick.setVisibility(View.GONE);
imageViewHotPot.setVisibility(View.GONE);
imageViewRice.setVisibility(View.GONE);
imageViewVegetable.setVisibility(View.GONE);
imageViewPotCover.setAlpha(1);
imageViewPotCover.setRotation(0);
float[] defaultPoint = {0, 0};
startParabolaAnimation(imageViewDrumstick, defaultPoint, defaultPoint, defaultPoint);
startParabolaAnimation(imageViewHotPot, defaultPoint, defaultPoint, defaultPoint);
startParabolaAnimation(imageViewRice, defaultPoint, defaultPoint, defaultPoint);
startParabolaAnimation(imageViewVegetable, defaultPoint, defaultPoint, defaultPoint);
}
@Override
public void onReset() {
rotated = false;
}
private void startAnimation() {
float x = 0;
float y = 0;
final float[] startPoint = {x, y};
Random random = new Random();
int nextInt = random.nextInt(50);
final float[] endPoint = {nextInt + 180, nextInt + 30};
final float[] midPoint = {nextInt + 100, nextInt - 70};
nextInt = random.nextInt(40);
final float[] endPoint2 = {nextInt + 160, nextInt + 40};
final float[] midPoint2 = {nextInt + 80, nextInt - 80};
nextInt = random.nextInt(30);
final float[] endPoint3 = {nextInt - 200, nextInt + 40};
final float[] midPoint3 = {nextInt - 100, nextInt - 70};
nextInt = random.nextInt(60);
final float[] endPoint4 = {nextInt - 170, nextInt + 45};
final float[] midPoint4 = {nextInt - 80, nextInt - 80};
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
startParabolaAnimation(imageViewDrumstick, startPoint, endPoint, midPoint);
}
}, 100);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
startParabolaAnimation(imageViewHotPot, startPoint, endPoint2, midPoint2);
}
}, 200);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
startParabolaAnimation(imageViewRice, startPoint, endPoint3, midPoint3);
}
}, 300);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
startParabolaAnimation(imageViewVegetable, startPoint, endPoint4, midPoint4);
}
}, 400);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
startAnimation();
}
}, 500);
}
/**
* 抛物线动画
*
* @param view
* @param startPoint 起点坐标
* @param endPoint 结束点坐标
* @param midPoint 中间点坐标
* @return
*/
public static ObjectAnimator startParabolaAnimation(final View view, float[] startPoint, float[] endPoint, float[] midPoint) {
//分200帧完成动画
int count = 200;
//动画时间持续600毫秒
int duration = 600;
Keyframe[] keyframes = new Keyframe[count];
final float keyStep = 1f / (float) count;
float key = keyStep;
//计算并保存每一帧x轴的位置
for (int i = 0; i < count; ++i) {
keyframes[i] = Keyframe.ofFloat(key, i * getDx(startPoint, endPoint) / count + startPoint[0]);
key += keyStep;
}
PropertyValuesHolder pvhX = PropertyValuesHolder.ofKeyframe("translationX", keyframes);
key = keyStep;
//计算并保存每一帧y轴的位置
for (int i = 0; i < count; ++i) {
keyframes[i] = Keyframe.ofFloat(key, getY(startPoint, endPoint, midPoint, i * getDx(startPoint, endPoint) / count + startPoint[0]));
key += keyStep;
}
PropertyValuesHolder pvhY = PropertyValuesHolder.ofKeyframe("translationY", keyframes);
ObjectAnimator yxBouncer = ObjectAnimator.ofPropertyValuesHolder(view, pvhY, pvhX).setDuration(duration);
//开始动画
yxBouncer.start();
return yxBouncer;
}
private static float getDx(float[] startPoint, float[] endPoint) {
return endPoint[0] - startPoint[0];
}
private static float getDy(float[] startPoint, float[] endPoint) {
return endPoint[1] - startPoint[1];
}
/**
* 这里是根据三个坐标点{startPoint,endPoint,midPoint}计算出来的抛物线方程
* y = ax² + bx + c
*
* @param x
* @return y
*/
private static float getY(float[] startPoint, float[] endPoint, float[] midPoint, float x) {
float x1 = startPoint[0];
float y1 = startPoint[1];
float x2 = endPoint[0];
float y2 = endPoint[1];
float x3 = midPoint[0];
float y3 = midPoint[1];
float a, b, c;
a = (y1 * (x2 - x3) + y2 * (x3 - x1) + y3 * (x1 - x2))
/ (x1 * x1 * (x2 - x3) + x2 * x2 * (x3 - x1) + x3 * x3 * (x1 - x2));
b = (y1 - y2) / (x1 - x2) - a * (x1 + x2);
c = y1 - (x1 * x1) * a - x1 * b;
return a * x * x + b * x + c;
}
}
第四步:布局文件中使用我们自定义控件,如下:
<neu.cn.swiperefreshdemo.customview.ELeMaRefreshHeaderView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="90dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal|bottom">
<ImageView
android:id="@+id/image_view_drumstick"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginBottom="24dp"
android:src="@drawable/icon_cook_01" />
<ImageView
android:id="@+id/image_view_hot_pot"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginBottom="24dp"
android:src="@drawable/icon_cook_02" />
<ImageView
android:id="@+id/image_view_rice"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginBottom="24dp"
android:src="@drawable/icon_cook_03" />
<ImageView
android:id="@+id/image_view_vegetable"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginBottom="24dp"
android:src="@drawable/icon_cook_04" />
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/image_view_pot_cover"
android:layout_width="50dp"
android:layout_height="30dp"
android:rotation="-0"
android:transformPivotX="0dp"
android:transformPivotY="30dp">
<ImageView
android:layout_width="50dp"
android:layout_height="18dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:scaleType="fitXY"
android:src="@drawable/icon_cook_pan_cover" />
</RelativeLayout>
<RelativeLayout
android:layout_width="60dp"
android:layout_height="30dp">
<ImageView
android:id="@+id/image_view_pot"
android:layout_width="60dp"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:scaleType="fitXY"
android:src="@drawable/icon_cook_pan" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="10dp"
android:src="@drawable/icon_cook_03" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="4dp"
android:layout_marginLeft="10dp"
android:src="@drawable/icon_cook_04" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="4dp"
android:layout_marginRight="10dp"
android:src="@drawable/icon_cook_01" />
<ImageView
android:layout_width="46dp"
android:layout_height="28dp"
android:layout_centerInParent="true"
android:scaleType="fitXY"
android:src="@drawable/icon_cook_water" />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
</neu.cn.swiperefreshdemo.customview.ELeMaRefreshHeaderView>
这里笔者为了减少布局的嵌套,使用了引入布局,在fragment_elema.xml中引入上面的布局文件,如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">
<com.aspsine.swipetoloadlayout.SwipeToLoadLayout
android:id="@+id/swipe_to_loadlayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@id/swipe_refresh_header"
layout="@layout/layout_elema_header" />
<TextView
android:id="@id/swipe_target"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1E90FF"
android:gravity="center"
android:text="下拉刷新"
android:textSize="24sp" />
</com.aspsine.swipetoloadlayout.SwipeToLoadLayout>
</RelativeLayout>
注意: SwipeToLoadLayout中布局包裹的view的id是指定的,笔者也不懂为何要指定,不要乱改就好啦!否者会找不到的;
<item name="swipe_target" type="id" /> 刷新目标
<item name="swipe_refresh_header" type="id" /> 刷新头部
<item name="swipe_load_more_footer" type="id" /> 刷新尾部
第五步:代码中加载fragment布局文件,并为SwipeToLoadLayout设置下拉刷新监听事件;
public class ELeMaRefreshFragment extends Fragment implements OnRefreshListener {
private SwipeToLoadLayout mSwipeToLoadLayout;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View mFragmentView = inflater.inflate(R.layout.fragment_elema, container, false);
mSwipeToLoadLayout = (SwipeToLoadLayout) mFragmentView.findViewById(R.id.swipe_to_loadlayout);
mSwipeToLoadLayout.setOnRefreshListener(this);
mSwipeToLoadLayout.postDelayed(new Runnable() {
@Override
public void run() {
mSwipeToLoadLayout.setRefreshing(true);
}
}, 100);
return mFragmentView;
}
@Override
public void onRefresh() {
mSwipeToLoadLayout.postDelayed(new Runnable() {
@Override
public void run() {
mSwipeToLoadLayout.setRefreshing(false);
}
}, 3000);
}
}
最后,在MainActivity中动态加载碎片,如下:
ELeMaRefreshFragment eLeMaRefreshFragment = new ELeMaRefreshFragment();
mFragmentManager.beginTransaction().replace(R.id.content_frame, eLeMaRefreshFragment).commit();
饿了吗下拉刷新效果如下所示:
笔者还写了其它六个下拉刷新效果,思路大致相同,这里简单展示几个,大家可以去下载源码,然后自己运行看一下效果,便于理解代码;
京东下拉刷新效果如下所示:
美团外卖下拉刷新效果如下所示:
百度外卖下拉刷新效果如下所示:
天猫下拉刷新效果如下所示:
完整代码:
更多效果展示及笔者demo的源代码,可以去笔者的GitHub中查看,欢迎大家下载;
源代码地址:https://github.com/henryneu/SwipeRefreshDemo