SpringView优点:
能对header/footer(头部/尾部)的样式和动画进行高度自定义,单独将header/footer独立出来,几乎可以实现任何你想要的效果,只需要继承BaseHeader/BaseFooter实现对应接口就可以。
能动态地替换header/footer,只需要设置不同的头尾即可:springView.setHeader(MyHeader());
在不重写源生控件的情况下,完美支持系统源生的listView、RecyclerView、ScrollView、WebView等,你依然使用google提供的官方控件,SpringView完全不做干涩。
使用简单,对于简单的需求甚至不用写任何代码,只需要在布局中为SpringView设置header=”@layout/…”属性即可。
SpringView是非常轻量级的实现,提供了非常容易拓展的对外接口
SpringView支持多点触控,可以两只手连续拖拽,你可以定制一些趣味的动画(例如demo5的仿acfun效果)
SpringView提供了2种拖拽方式(重叠和跟随),可以动态地切换
SpringView为不想去自定义头/尾的懒人提供了7种默认的实现(模仿了阿里,腾讯,美团等多种风格)如下,还会继续增加
基本使用方式
(1)添加依赖
compile 'com.liaoinstan.springview:library:1.2.6'
(2)在布局文件中添加SpringView,注意SpringView和ScrollView有同样的限制:只能有一个子元素
并把你想要拖拽的控件放在SpringView中,给SpringView添加app:header=”@layout/…”属性,设置一个自己编写的头部的布局即可(footer同理):
<com.liaoinstan.springview.widget.SpringView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:header="@layout/myheader"
app:footer="@layout/myfooter">
<listView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.liaoinstan.springview.widget.SpringView>
当然,你也可以不再布局中设置,使用代码动态添加:
//DefaultHeader/Footer是SpringView已经实现的默认头/尾之一
//更多还有MeituanHeader、AliHeader、RotationHeader等如上图7种
springView.setHeader(new DefaultHeader(this));
springView.setFooter(new DefaultFooter(this));
刷新和加载更多的事件处理
如果需要处理的话,只需在代码中添加监听:
springView.setListener(new SpringView.OnFreshListener() {
@Override
public void onRefresh() {
Toast.makeText(getApplicationContext(),"下拉刷新中",Toast.LENGTH_SHORT).show();
// list.clear();
// 网络请求;
// mStarFragmentPresenter.queryData();
//一分钟之后关闭刷新的方法
finishFreshAndLoad();
}
@Override
public void onLoadmore() {
Toast.makeText(getApplicationContext(),"玩命加载中...",Toast.LENGTH_SHORT).show();
finishFreshAndLoad();
}
});
在刷新和加载更多的时候等待1秒调用onFinishFreshAndLoad()结束刷新动作
/**
* 关闭加载提示
*/
private void finishFreshAndLoad() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
spRefresh.onFinishFreshAndLoad();
}
}, 1000);
}
以上就是它的基本使用方式;
我们可以看一下它的几个默认的加载效果实现;
(1)第一个效果;
这是它添加的默认的下拉头和上拉加载;
springView.setHeader(new DefaultHeader(this));
springView.setFooter(new DefaultFooter(this));
它的源代码;
/**
* 通过类继承BaseHeader来实现动态添加头 ;
*/
public class DefaultHeader extends BaseHeader {
//传进来的上下文;
private Context context;
//旋转动画的int
private int rotationSrc;
//箭头
private int arrowSrc;
//定义刷新时间的标记;
private long freshTime;
//动画执行时间180mm
private final int ROTATE_ANIM_DURATION = 180;
//箭头朝上的动画;
private RotateAnimation mRotateUpAnim;
//箭头朝下的动画;
private RotateAnimation mRotateDownAnim;
private TextView headerTitle;
private TextView headerTime;
private ImageView headerArrow;
private ProgressBar headerProgressbar;
/**
* 构造方法中传进来上下文,然后传递给三个参数的构造方法;
* @param context
*/
public DefaultHeader(Context context){
//第一个参数是上下文,第二个参数是加载的圆圈,其中是加载一个旋转的动画,第三个参数是箭头;
this(context, R.drawable.progress_small,R.drawable.arrow);
}
public DefaultHeader(Context context,int rotationSrc,int arrowSrc){
this.context = context;
this.rotationSrc = rotationSrc;
this.arrowSrc = arrowSrc;
//添加一个逆时针旋转动画使朝下的箭头转动到朝上;
mRotateUpAnim = new RotateAnimation(0.0f, -180.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION);
//动画执行完停在当前位置;
mRotateUpAnim.setFillAfter(true);
//添加一个顺时针旋转动画使朝上的箭头转动到朝下;
mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION);
mRotateDownAnim.setFillAfter(true);
}
/***
* //获取Header;
* @param inflater
* @param viewGroup
* @return
*/
@Override
public View getView(LayoutInflater inflater,ViewGroup viewGroup) {
//添加头部的布局view;
View view = inflater.inflate(R.layout.default_header, viewGroup, true);
//头部标题
headerTitle = (TextView) view.findViewById(R.id.default_header_title);
//记录刷新的时间;
headerTime = (TextView) view.findViewById(R.id.default_header_time);
//箭头;
headerArrow = (ImageView) view.findViewById(R.id.default_header_arrow);
//加载的圆圈;
headerProgressbar = (ProgressBar) view.findViewById(R.id.default_header_progressbar);
//将int加入到控件上;
headerProgressbar.setIndeterminateDrawable(ContextCompat.getDrawable(context, rotationSrc));
headerArrow.setImageResource(arrowSrc);
return view;
}
/**
* 拖拽开始前回调
* 设置头部的控件时间
* @param rootView
*/
@Override
public void onPreDrag(View rootView) {
//如果刷新时间为零.表示没有刷新过;
if (freshTime==0){
//获取到当前的运行时间;
freshTime = System.currentTimeMillis();
}else {
//如果不是第一次刷新,获取当前刷新时间然后减掉-上次一刷新的时间
int m = (int) ((System.currentTimeMillis()-freshTime)/1000/60);
if(m>=1 && m<60){
headerTime.setText( m +"分钟前");
}else if (m>=60){
int h = m/60;
headerTime.setText( h +"小时前");
}else if(m>60*24){
int d = m/(60*24);
headerTime.setText( d +"天前");
}else if(m==0){
headerTime.setText("刚刚");
}
}
}
/***
* //手指拖拽过程中不断回调,dy为拖拽的距离,可以根据拖动的距离添加拖动过程动画
* @param rootView
* @param dy 拖动距离,下拉为+,上拉为-
*/
@Override
public void onDropAnim(View rootView, int dy) {
}
/***
* //手指拖拽过程中每次经过临界点时回调,upORdown是向上经过还是向下经过
* @param rootView
* @param upORdown 是上拉还是下拉
*/
@Override
public void onLimitDes(View rootView, boolean upORdown) {
//upORdown 为false时表示过了临界点
if (!upORdown){
//修改标题信息;
headerTitle.setText("松开刷新数据");
//判断箭头可见
if (headerArrow.getVisibility()==View.VISIBLE)
//加入向上的动画;
headerArrow.startAnimation(mRotateUpAnim);
}
else {
//没过临界点,还是下拉刷新
headerTitle.setText("下拉刷新");
if (headerArrow.getVisibility()==View.VISIBLE)
//动画转为向下;
headerArrow.startAnimation(mRotateDownAnim);
}
}
/***
* //拉动超过临界点后松开时回调
*/
@Override
public void onStartAnim() {
//再一次记录刷新的时间;
freshTime = System.currentTimeMillis();
//设置标题信息是正在刷新;
headerTitle.setText("正在刷新");
//隐藏箭头;
headerArrow.setVisibility(View.INVISIBLE);
//清除箭头动画;
headerArrow.clearAnimation();
//设置加载圈进行显示;
headerProgressbar.setVisibility(View.VISIBLE);
}
/**
* //头部已经全部弹回时回调,也就是头部不可见的时候
*/
@Override
public void onFinishAnim() {
//显示箭头
headerArrow.setVisibility(View.VISIBLE);
//隐藏圆圈的加载动画;
headerProgressbar.setVisibility(View.INVISIBLE);
}
}
上拉加载的默认footer
/**
* 默认的上啦加载;
*/
public class DefaultFooter extends BaseFooter {
private Context context;
private int rotationSrc;
private TextView footerTitle;
private ProgressBar footerProgressbar;
public DefaultFooter(Context context){
this(context,R.drawable.progress_small);
}
public DefaultFooter(Context context,int rotationSrc){
this.context = context;
this.rotationSrc = rotationSrc;
}
@Override
public View getView(LayoutInflater inflater, ViewGroup viewGroup) {
View view = inflater.inflate(R.layout.default_footer, viewGroup, true);
footerTitle = (TextView) view.findViewById(R.id.default_footer_title);
footerProgressbar = (ProgressBar) view.findViewById(R.id.default_footer_progressbar);
footerProgressbar.setIndeterminateDrawable(ContextCompat.getDrawable(context,rotationSrc));
return view;
}
/**
* 拖拽开始前回调
* 设置头部的控件时间
* @param rootView
*/
@Override
public void onPreDrag(View rootView) {
}
/***
* //手指拖拽过程中不断回调,dy为拖拽的距离,可以根据拖动的距离添加拖动过程动画
* @param rootView
* @param dy 拖动距离,下拉为+,上拉为-
*/
@Override
public void onDropAnim(View rootView, int dy) {
}
/***
* //手指拖拽过程中每次经过临界点时回调,upORdown是向上经过还是向下经过
* @param rootView
* @param upORdown 是上拉还是下拉
*/
@Override
public void onLimitDes(View rootView, boolean upORdown) {
if (upORdown) {
footerTitle.setText("松开载入更多");
} else {
footerTitle.setText("查看更多");
}
}
/***
* //拉动超过临界点后松开时回调
*/
@Override
public void onStartAnim() {
footerTitle.setVisibility(View.INVISIBLE);
footerProgressbar.setVisibility(View.VISIBLE);
}
/**
* //头部已经全部弹回时回调,也就是头部不可见的时候
*/
@Override
public void onFinishAnim() {
footerTitle.setText("查看更多");
footerTitle.setVisibility(View.VISIBLE);
footerProgressbar.setVisibility(View.INVISIBLE);
}
}
第二种效果仿美团上拉和下拉;
//第一个参数上下文,第二个参数下拉拖拽是的动画int,第三个参数是放开刷新的动画int
springView.setHeader(new MeituanHeader(this,pullAnimSrcs,refreshAnimSrcs));
springView.setFooter(new MeituanFooter(this,loadingAnimSrcs));
它的源代码;
public class MeituanHeader extends BaseHeader {
private AnimationDrawable animationPull;
private AnimationDrawable animationPullFan;
private AnimationDrawable animationRefresh;
private Context context;
private ImageView header_img;
private int[] pullAnimSrcs = new int[]{R.drawable.mt_pull,R.drawable.mt_pull01,R.drawable.mt_pull02,R.drawable.mt_pull03,R.drawable.mt_pull04,R.drawable.mt_pull05};
private int[] refreshAnimSrcs = new int[]{R.drawable.mt_refreshing01,R.drawable.mt_refreshing02,R.drawable.mt_refreshing03,R.drawable.mt_refreshing04,R.drawable.mt_refreshing05,R.drawable.mt_refreshing06};
public MeituanHeader(Context context){
this(context,null,null);
}
/***
* 可以直接传入下拉时的动画数组,以及刷新动画的数组;
* @param context
* @param pullAnimSrcs
* @param refreshAnimSrcs
*/
public MeituanHeader(Context context,int[] pullAnimSrcs,int[] refreshAnimSrcs){
this.context = context;
if (pullAnimSrcs!=null) this.pullAnimSrcs = pullAnimSrcs;
if (refreshAnimSrcs!=null) this.refreshAnimSrcs = refreshAnimSrcs;
// Drawable animation可以加载Drawable资源实现帧动画。AnimationDrawable是实现Drawable animations的基本类
//直接在代码中动态添加frame:帧动画;
animationPull = new AnimationDrawable();
animationPullFan = new AnimationDrawable();
animationRefresh = new AnimationDrawable();
for (int i=1;i< this.pullAnimSrcs.length;i++) {
//第一个参数是drawable,第二个参数是动画时间
//ContextCompat.getDrawable使用默认的activity主题加载drawable
animationPull.addFrame(ContextCompat.getDrawable(context, this.pullAnimSrcs[i]),100);
// 设置Android:oneshot属性为true,表示此次动画只执行一次,最后停留在最后一帧。设置为false则动画循环播放。文件可以添加为Image背景,触发的时候播放。
animationRefresh.setOneShot(true);
}
for (int i= this.pullAnimSrcs.length-1;i>=0;i--){
animationPullFan.addFrame(ContextCompat.getDrawable(context, this.pullAnimSrcs[i]), 100);
animationRefresh.setOneShot(true);
}
for (int src: this.refreshAnimSrcs) {
animationRefresh.addFrame(ContextCompat.getDrawable(context, src),150);
animationRefresh.setOneShot(false);
}
}
/***
* //获取Header;
* @param inflater
* @param viewGroup
* @return
*/
@Override
public View getView(LayoutInflater inflater,ViewGroup viewGroup) {
View view = inflater.inflate(R.layout.meituan_header, viewGroup, true);
//初始化控件image;
header_img = (ImageView) view.findViewById(R.id.meituan_header_img);
if (pullAnimSrcs !=null&& pullAnimSrcs.length>0)
header_img.setImageResource(pullAnimSrcs[0]);
return view;
}
/**
* 拖拽开始前回调
* 设置头部的控件时间
* @param rootView
*/
@Override
public void onPreDrag(View rootView) {
}
/***
* //手指拖拽过程中不断回调,dy为拖拽的距离,可以根据拖动的距离添加拖动过程动画
* @param rootView
* @param dy 拖动距离,下拉为+,上拉为-
*/
@Override
public void onDropAnim(View rootView, int dy) {
//dp像素转换成px这是移动的最大高度
int maxw = DensityUtil.dip2px(context, 45);
//计算出移动的绝对值高度;
float w = maxw*Math.abs(dy)/rootView.getMeasuredHeight();
//超出最大高度就结束方法;
if (w>maxw) return;
//获取到图片控件的父类布局;
ViewGroup.LayoutParams layoutParams = header_img.getLayoutParams();
//不断的赋值给父类布局的高度;
layoutParams.width = (int) w;
//然后将新的数据设置给控件
header_img.setLayoutParams(layoutParams);
}
/***
* //手指拖拽过程中每次经过临界点时回调,upORdown是向上经过还是向下经过
* @param rootView
* @param upORdown 是上拉还是下拉
*/
@Override
public void onLimitDes(View rootView, boolean upORdown) {
if (!upORdown){
//没突破临界点的操作;
header_img.setImageDrawable(animationPull);
animationPull.start();
}else {
//突破临界点的操作;
header_img.setImageDrawable(animationPullFan);
animationPullFan.start();
}
}
/***
* //拉动超过临界点后松开时回调
*/
@Override
public void onStartAnim() {
header_img.setImageDrawable(animationRefresh);
animationRefresh.start();
}
/**
* //头部已经全部弹回时回调,也就是头部不可见的时候
*/
@Override
public void onFinishAnim() {
if (pullAnimSrcs !=null&& pullAnimSrcs.length>0)
header_img.setImageResource(pullAnimSrcs[0]);
}
}
上拉加载
public class MeituanFooter extends BaseFooter {
private AnimationDrawable animationLoading;
private Context context;
private ImageView footer_img;
private int[] loadingAnimSrcs = new int[]{R.drawable.mt_loading01,R.drawable.mt_loading02};
public MeituanFooter(Context context){
this(context,null);
}
public MeituanFooter(Context context,int[] loadingAnimSrcs){
this.context = context;
if (loadingAnimSrcs!=null) this.loadingAnimSrcs = loadingAnimSrcs;
animationLoading = new AnimationDrawable();
for (int src: this.loadingAnimSrcs) {
animationLoading.addFrame(ContextCompat.getDrawable(context, src),150);
animationLoading.setOneShot(false);
}
}
@Override
public View getView(LayoutInflater inflater,ViewGroup viewGroup) {
View view = inflater.inflate(R.layout.meituan_footer, viewGroup, true);
footer_img = (ImageView) view.findViewById(R.id.meituan_footer_img);
if (animationLoading!=null)
footer_img.setImageDrawable(animationLoading);
return view;
}
@Override
public void onPreDrag(View rootView) {
animationLoading.stop();
if (animationLoading!=null && animationLoading.getNumberOfFrames()>0)
footer_img.setImageDrawable(animationLoading.getFrame(0));
}
@Override
public void onDropAnim(View rootView, int dy) {
}
@Override
public void onLimitDes(View rootView, boolean upORdown) {
}
@Override
public void onStartAnim() {
if (animationLoading!=null)
footer_img.setImageDrawable(animationLoading);
animationLoading.start();
}
@Override
public void onFinishAnim() {
animationLoading.stop();
if (animationLoading!=null && animationLoading.getNumberOfFrames()>0)
footer_img.setImageDrawable(animationLoading.getFrame(0));
}
}
因此自己定义时的方式
如何自定义一个Header或Footer
详细的你可以看下几种默认实现的Header源码,这里只简单介绍下:
我们要做一个简单的能够记录拖拽了多少次的Header。
首先新建一个自定义头部布局,就是在RelativeLayout中放一个TextView啦:
这里写图片描述
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@drawable/view_post_comment_bg"
android:layout_gravity="top">
<TextView
android:layout_width="120dp"
android:gravity="center"
android:layout_height="wrap_content"
android:text="this is TextView "
android:background="#ff0000"
android:textColor="#ffffff"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:id="@+id/textView" />
</RelativeLayout>
接着,新建一个MyHeader继承自BaseHeader:
public class MyHeader extends BaseHeader{
//获取Header
@Override
public View getView(LayoutInflater inflater, ViewGroup viewGroup) {}
//拖拽开始前回调
@Override
public void onPreDrag(View rootView) {}
//手指拖拽过程中不断回调,dy为拖拽的距离,可以根据拖动的距离添加拖动过程动画
@Override
public void onDropAnim(View rootView, int dy) {}
//手指拖拽过程中每次经过临界点时回调,upORdown是向上经过还是向下经过
@Override
public void onLimitDes(View rootView, boolean upORdown) {}
//拉动超过临界点后松开时回调
@Override
public void onStartAnim() {}
//头部已经全部弹回时回调
@Override
public void onFinishAnim() {}
}
注释已经很清楚了,其中必须实现的方法是getView(),将头部的View实例返回给SpringView,其余方法有需求就实现。
实现getView()方法,添加一个成员变量保存头部中的TextView:
private TextView textView;
@Override
public View getView(LayoutInflater inflater, ViewGroup viewGroup) {
View view = inflater.inflate(R.layout.header_my, viewGroup, true);
textView = (TextView)view.findViewById(R.id.textView);
return view;
}
实现onLimitDes方法,在每次经过临界点时改变TextView的内容:
private int i = 0;
@Override
public void onLimitDes(View rootView, boolean upORdown) {
i++;
textView.setText("this is TextView "+i);
}
OK,在Activity中为SpringView设置我们自定义的MyHeader就可以了,再设置一个监听器,在刷新和加载更多的时候等待1秒调用onFinishFreshAndLoad()结束刷新动作。
springView = (SpringView) findViewById(R.id.springview);
springView.setHeader(new MyHeader());
springView.setListener(new SpringView.OnFreshListener() {
@Override
public void onRefresh() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
springView.onFinishFreshAndLoad();
}
}, 1000);
}
@Override
public void onLoadmore() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
springView.onFinishFreshAndLoad();
}
}, 1000);
}
});
如何自定义最大下拉高度,临界高度,和回弹高度
在BaseHeader(BaseFooter同理)中默认已经实现3个方法,分别返回的是临界高度(limit hight),下拉最大高度(max height),下拉弹动高度(spring height):
如果有更加复杂的需求,需要更改这些高度的话,就在自己的Header中重写这些方法,注释已经很清楚了:
public abstract class BaseHeader implements SpringView.DragHander{
/**
* 这个方法用于设置当前View的临界高度(limit hight),即拉动到多少会被认定为刷新超作,而没到达该高度则不会执行刷新
* 返回值大于0才有效,如果<=0 则设置为默认header的高度
* 默认返回0
*/
@Override
public int getDragLimitHeight(View rootView) {
return 0;
}
/**
* 这个方法用于设置下拉最大高度(max height),无论怎么拉动都不会超过这个高度
* 返回值大于0才有效,如果<=0 则默认600px
* 默认返回0
*/
@Override
public int getDragMaxHeight(View rootView) {
return 0;
}
/**
* 这个方法用于设置下拉弹动高度(spring height),即弹动后停止状态的高度
* 返回值大于0才有效,如果<=0 则设置为默认header的高度
* 默认返回0
*/
@Override
public int getDragSpringHeight(View rootView) {
return 0;
}
}