我们先来看看Android中的动画吧:
Android中的动画分为三种:
Tween动画,这一类的动画提供了旋转、平移、缩放等效果。
Alpha – 淡入淡出
Scale – 缩放效果
Roate – 旋转效果
Translate – 平移效果
Frame动画(帧动画),这一类动画可以创建一个Drawable序列,按照指定时间间歇一个一个显示出来。
Property动画(属性动画),Android3.0之后引入出来的属性动画,它更改的是对象的实际属性。
我们可以看到百度外卖的下拉刷新的头是一个骑车的快递员在路上疾行,分析一下我们得到下面的动画:
背景图片的平移动画
太阳的自旋转动画
两个小轮子的自旋转动画
这就很简单了,接下来我们去百度外面的图片资源文件里找到这几张图片:(下载百度外卖的apk直接解压即可)
定义下拉刷新头文件:headview.xml
这里注意一下:我们定义了两张背景图片的ImageView是为了可以实现背景的平移动画效果。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_back1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:src="@drawable/pull_back" />
<ImageView
android:id="@+id/iv_back2"
android:layout_width="match_parent"
android:layout_height="100dp"
android:src="@drawable/pull_back" />
<RelativeLayout
android:id="@+id/main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/iv_rider"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="45dp"
android:background="@drawable/pull_rider" />
<ImageView
android:id="@+id/wheel1"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="90dp"
android:background="@drawable/pull_wheel" />
<ImageView
android:id="@+id/wheel2"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginLeft="40dp"
android:layout_marginTop="90dp"
android:background="@drawable/pull_wheel" />
RelativeLayout>
<ImageView
android:id="@+id/ivsun"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="20dp"
android:layout_toRightOf="@+id/main"
android:background="@drawable/pull_sun" />
RelativeLayout>
接下来我们定义动画效果:
背景图片的平移效果:
实现两个animation xml文件,一个起始位置在100%,结束位置在0%,设置repeat属性为循环往复。
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
<translate android:fromXDelta="100%p"
android:toXDelta="0%p"
android:repeatMode="restart"
android:interpolator="@android:anim/linear_interpolator"
android:repeatCount="infinite" android:duration="5000" />
set>
另一个起始位置在0%,结束位置在-100%
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
<translate android:fromXDelta="0%p"
android:toXDelta="-100%p"
android:repeatMode="restart" android:interpolator="@android:anim/linear_interpolator" android:repeatCount="infinite"
android:duration="5000" />
set>
太阳围绕中心旋转动画:
从0-360度开始循环旋转,旋转所用时间为1s,旋转中心距离view的左定点上边缘为50%的距离,也就是正中心。
下面是具体属性:
android:fromDegrees 起始的角度度数
android:toDegrees 结束的角度度数,负数表示逆时针,正数表示顺时针。如10圈则比android:fromDegrees大3600即可
android:pivotX 旋转中心的X坐标
浮点数或是百分比。浮点数表示相对于Object的左边缘,如5; 百分比表示相对于Object的左边缘,如5%; 另一种百分比表示相对于父容器的左边缘,如5%p; 一般设置为50%表示在Object中心
android:pivotY 旋转中心的Y坐标
浮点数或是百分比。浮点数表示相对于Object的上边缘,如5; 百分比表示相对于Object的上边缘,如5%; 另一种百分比表示相对于父容器的上边缘,如5%p; 一般设置为50%表示在Object中心
android:duration 表示从android:fromDegrees转动到android:toDegrees所花费的时间,单位为毫秒。可以用来计算速度。
android:interpolator表示变化率,但不是运行速度。一个插补属性,可以将动画效果设置为加速,减速,反复,反弹等。默认为开始和结束慢中间快,
android:startOffset 在调用start函数之后等待开始运行的时间,单位为毫秒,若为10,表示10ms后开始运行
android:repeatCount 重复的次数,默认为0,必须是int,可以为-1表示不停止
android:repeatMode 重复的模式,默认为restart,即重头开始重新运行,可以为reverse即从结束开始向前重新运行。在android:repeatCount大于0或为infinite时生效
android:detachWallpaper 表示是否在壁纸上运行
android:zAdjustment 表示被animated的内容在运行时在z轴上的位置,默认为normal。
normal保持内容当前的z轴顺序
top运行时在最顶层显示
bottom运行时在最底层显示
<set xmlns:android="http://schemas.android.com/apk/res/android">
<rotate android:fromDegrees="0"
android:toDegrees="360"
android:duration="1000"
android:repeatCount="-1"
android:pivotX="50%"
android:pivotY="50%" />
set>
同理轮子的动画也一样,不占代码了。
动画定义完了我们开始定义下拉刷新列表,下拉刷新网上有很多,不详细的说了,简单的改造一下,根据刷新状态开启关闭动画即可。
public class BaiDuRefreshListView extends ListView implements AbsListView.OnScrollListener{
private static final int DONE = 0; //刷新完毕状态
private static final int PULL_TO_REFRESH = 1; //下拉刷新状态
private static final int RELEASE_TO_REFRESH = 2; //释放状态
private static final int REFRESHING = 3; //正在刷新状态
private static final int RATIO = 3;
private RelativeLayout headView; //下拉刷新头
private int headViewHeight; //头高度
private float startY; //开始Y坐标
private float offsetY; //Y轴偏移量
private OnBaiduRefreshListener mOnRefreshListener; //刷新接口
private int state; //状态值
private int mFirstVisibleItem; //第一项可见item索引
private boolean isRecord; //是否记录
private boolean isEnd; //是否结束
private boolean isRefreable; //是否刷新
private ImageView ivWheel1,ivWheel2; //轮组图片组件
private ImageView ivRider; //骑手图片组件
private ImageView ivSun,ivBack1,ivBack2; //太阳、背景图片1、背景图片2
private Animation wheelAnimation,sunAnimation; //轮子、太阳动画
private Animation backAnimation1,backAnimation2; //两张背景图动画
public BaiDuRefreshListView(Context context) {
super(context);
init(context);
}
public BaiDuRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public BaiDuRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public interface OnBaiduRefreshListener{
void onRefresh();
}
/** * 回调接口,想实现下拉刷新的listview实现此接口 * @param onRefreshListener */
public void setOnBaiduRefreshListener(OnBaiduRefreshListener onRefreshListener){
mOnRefreshListener = onRefreshListener;
isRefreable = true;
}
/** * 刷新完毕,从主线程发送过来,并且改变headerView的状态和文字动画信息 */
public void setOnRefreshComplete(){
//一定要将isEnd设置为true,以便于下次的下拉刷新
isEnd = true;
state = DONE;
changeHeaderByState(state);
}
private void init(Context context) {
//关闭view的OverScroll
setOverScrollMode(OVER_SCROLL_NEVER);
setOnScrollListener(this);
//加载头布局
headView = (RelativeLayout) LayoutInflater.from(context).inflate(R.layout.headview,this,false);
//测量头布局
measureView(headView);
//给ListView添加头布局
addHeaderView(headView);
//设置头文件隐藏在ListView的第一项
headViewHeight = headView.getMeasuredHeight();
headView.setPadding(0, -headViewHeight, 0, 0);
//获取头布局图片组件
ivRider = (ImageView) headView.findViewById(R.id.iv_rider);
ivSun = (ImageView) headView.findViewById(R.id.ivsun);
ivWheel1 = (ImageView) headView.findViewById(R.id.wheel1);
ivWheel2 = (ImageView) headView.findViewById(R.id.wheel2);
ivBack1 = (ImageView) headView.findViewById(R.id.iv_back1);
ivBack2 = (ImageView) headView.findViewById(R.id.iv_back2);
//获取动画
wheelAnimation = AnimationUtils.loadAnimation(context, R.anim.tip);
sunAnimation = AnimationUtils.loadAnimation(context, R.anim.tip1);
backAnimation1 = AnimationUtils.loadAnimation(context, R.anim.a);
backAnimation2 = AnimationUtils.loadAnimation(context, R.anim.b);
state = DONE;
isEnd = true;
isRefreable = false;
}
@Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
@Override
public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mFirstVisibleItem = firstVisibleItem;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (isEnd) {//如果现在时结束的状态,即刷新完毕了,可以再次刷新了,在onRefreshComplete中设置
if (isRefreable) {//如果现在是可刷新状态 在setOnMeiTuanListener中设置为true
switch (ev.getAction()){
//用户按下
case MotionEvent.ACTION_DOWN:
//如果当前是在listview顶部并且没有记录y坐标
if (mFirstVisibleItem == 0 && !isRecord) {
//将isRecord置为true,说明现在已记录y坐标
isRecord = true;
//将当前y坐标赋值给startY起始y坐标
startY = ev.getY();
}
break;
//用户滑动
case MotionEvent.ACTION_MOVE:
//再次得到y坐标,用来和startY相减来计算offsetY位移值
float tempY = ev.getY();
//再起判断一下是否为listview顶部并且没有记录y坐标
if (mFirstVisibleItem == 0 && !isRecord) {
isRecord = true;
startY = tempY;
}
//如果当前状态不是正在刷新的状态,并且已经记录了y坐标
if (state!=REFRESHING && isRecord ) {
//计算y的偏移量
offsetY = tempY - startY;
//计算当前滑动的高度
float currentHeight = (-headViewHeight+offsetY/3);
//用当前滑动的高度和头部headerView的总高度进行比 计算出当前滑动的百分比 0到1
float currentProgress = 1+currentHeight/headViewHeight;
//如果当前百分比大于1了,将其设置为1,目的是让第一个状态的椭圆不再继续变大
if (currentProgress>=1) {
currentProgress = 1;
}
//如果当前的状态是放开刷新,并且已经记录y坐标
if (state == RELEASE_TO_REFRESH && isRecord) {
setSelection(0);
//如果当前滑动的距离小于headerView的总高度
if (-headViewHeight+offsetY/RATIO<0) {
//将状态置为下拉刷新状态
state = PULL_TO_REFRESH;
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
//如果当前y的位移值小于0,即为headerView隐藏了
}else if (offsetY<=0) {
//将状态变为done
state = DONE;
stopAnim();
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
}
}
//如果当前状态为下拉刷新并且已经记录y坐标
if (state == PULL_TO_REFRESH && isRecord) {
setSelection(0);
//如果下拉距离大于等于headerView的总高度
if (-headViewHeight+offsetY/RATIO>=0) {
//将状态变为放开刷新
state = RELEASE_TO_REFRESH;
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
//如果当前y的位移值小于0,即为headerView隐藏了
}else if (offsetY<=0) {
//将状态变为done
state = DONE;
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
}
}
//如果当前状态为done并且已经记录y坐标
if (state == DONE && isRecord) {
//如果位移值大于0
if (offsetY>=0) {
//将状态改为下拉刷新状态
state = PULL_TO_REFRESH;
changeHeaderByState(state);
}
}
//如果为下拉刷新状态
if (state == PULL_TO_REFRESH) {
//则改变headerView的padding来实现下拉的效果
headView.setPadding(0,(int)(-headViewHeight+offsetY/RATIO) ,0,0);
}
//如果为放开刷新状态
if (state == RELEASE_TO_REFRESH) {
//改变headerView的padding值
headView.setPadding(0,(int)(-headViewHeight+offsetY/RATIO) ,0, 0);
}
}
break;
//当用户手指抬起时
case MotionEvent.ACTION_UP:
//如果当前状态为下拉刷新状态
if (state == PULL_TO_REFRESH) {
//平滑的隐藏headerView
this.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO)+headViewHeight, 500);
//根据状态改变headerView
changeHeaderByState(state);
}
//如果当前状态为放开刷新
if (state == RELEASE_TO_REFRESH) {
//平滑的滑到正好显示headerView
this.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO), 500);
//将当前状态设置为正在刷新
state = REFRESHING;
//回调接口的onRefresh方法
mOnRefreshListener.onRefresh();
//根据状态改变headerView
changeHeaderByState(state);
}
//这一套手势执行完,一定别忘了将记录y坐标的isRecord改为false,以便于下一次手势的执行
isRecord = false;
break;
}
}
}
return super.onTouchEvent(ev);
}
/** * 根据状态改变headerView的动画和文字显示 * @param state */
private void changeHeaderByState(int state){
switch (state) {
case DONE://如果的隐藏的状态
//设置headerView的padding为隐藏
headView.setPadding(0, -headViewHeight, 0, 0);
startAnim();
break;
case RELEASE_TO_REFRESH://当前状态为放开刷新
break;
case PULL_TO_REFRESH://当前状态为下拉刷新
startAnim();
break;
case REFRESHING://当前状态为正在刷新
break;
default:
break;
}
}
/** * 测量View * @param child */
private void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
/** * 开启动画 */
public void startAnim(){
ivBack1.startAnimation(backAnimation1);
ivBack2.startAnimation(backAnimation2);
ivSun.startAnimation(sunAnimation);
ivWheel1.startAnimation(wheelAnimation);
ivWheel2.startAnimation(wheelAnimation);
}
/** * 关闭动画 */
public void stopAnim(){
ivBack1.clearAnimation();
ivBack2.clearAnimation();
ivSun.clearAnimation();
ivWheel1.clearAnimation();
ivWheel2.clearAnimation();
}
}
美团的下拉刷新分为三个状态:
第一个状态为下拉刷新状态(pull to refresh),在这个状态下是一个绿色的椭圆随着下拉的距离动态改变其大小。
第二个部分为放开刷新状态(release to refresh),在这个状态下是一个帧动画,效果为从躺着变为站起来的动画。
第三个部分为刷新状态(refreshing),在这个状态下也是一个帧动画,是摇头的动画。
其中第二和第三个状态很简单,就是两个帧动画,第一个状态我们可以用自定义View来实现。
第一个状态的实现:
我们的思路是:当前这个椭圆形有一个进度值,这个进度值从0变为1,然后对这个椭圆形进行缩放,我们可以使用自定义View来实现这个效果。
MeiTuanRefreshFirstStepView
public class MeiTuanRefreshFirstStepView extends View{
private Bitmap initialBitmap;
private int measuredWidth;
private int measuredHeight;
private Bitmap endBitmap;
private float mCurrentProgress;
private Bitmap scaledBitmap;
public MeiTuanRefreshFirstStepView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public MeiTuanRefreshFirstStepView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MeiTuanRefreshFirstStepView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
//这个就是那个椭圆形图片
initialBitmap = Bitmap.createBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.pull_image));
//这个是第二个状态娃娃的图片,之所以要这张图片,是因为第二个状态和第三个状态的图片的大小是一致的,而第一阶段
//椭圆形图片的大小与第二阶段和第三阶段不一致,因此我们需要根据这张图片来决定第一张图片的宽高,来保证
//第一阶段和第二、三阶段的View的宽高一致
endBitmap = Bitmap.createBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.pull_end_image_frame_05));
}
/**
* 重写onMeasure方法主要是设置wrap_content时 View的大小
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//根据设置的宽度来计算高度 设置为符合第二阶段娃娃图片的宽高比例
setMeasuredDimension(measureWidth(widthMeasureSpec),measureWidth(widthMeasureSpec)*endBitmap.getHeight()/endBitmap.getWidth());
}
/**
* 当wrap_content的时候,宽度即为第二阶段娃娃图片的宽度
* @param widMeasureSpec
* @return
*/
private int measureWidth(int widMeasureSpec){
int result = 0;
int size = MeasureSpec.getSize(widMeasureSpec);
int mode = MeasureSpec.getMode(widMeasureSpec);
if (mode == MeasureSpec.EXACTLY){
result = size;
}else{
result = endBitmap.getWidth();
if (mode == MeasureSpec.AT_MOST){
result = Math.min(result,size);
}
}
return result;
}
/**
* 在onLayout里面获得测量后View的宽高
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
measuredWidth = getMeasuredWidth();
measuredHeight = getMeasuredHeight();
//根据第二阶段娃娃宽高 给椭圆形图片进行等比例的缩放
scaledBitmap = Bitmap.createScaledBitmap(initialBitmap, measuredWidth,measuredWidth*initialBitmap.getHeight()/initialBitmap.getWidth(), true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//这个方法是对画布进行缩放,从而达到椭圆形图片的缩放,第一个参数为宽度缩放比例,第二个参数为高度缩放比例,
canvas.scale(mCurrentProgress, mCurrentProgress, measuredWidth/2, measuredHeight/2);
//将等比例缩放后的椭圆形画在画布上面
canvas.drawBitmap(scaledBitmap,0,measuredHeight/4,null);
}
/**
* 设置缩放比例,从0到1 0为最小 1为最大
* @param currentProgress
*/
public void setCurrentProgress(float currentProgress){
mCurrentProgress = currentProgress;
}
}
第二个状态的实现:
第二个状态是一个帧动画,我们为了保证View大小的统一,我们也进行自定义View,这个自定义View很简单,只是为了和第一阶段View的宽高保证一致即可。
public class MeiTuanRefreshSecondStepView extends View{
private Bitmap endBitmap;
public MeiTuanRefreshSecondStepView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init();
}
public MeiTuanRefreshSecondStepView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MeiTuanRefreshSecondStepView(Context context) {
super(context);
init();
}
private void init() {
endBitmap = Bitmap.createBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.pull_end_image_frame_05));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), measureWidth(widthMeasureSpec)*endBitmap.getHeight()/endBitmap.getWidth());
}
private int measureWidth(int widthMeasureSpec){
int result = 0;
int size = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (mode == MeasureSpec.EXACTLY) {
result = size;
}else {
result = endBitmap.getWidth();
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
}
return result;
}
}
我们用xml定义一组帧动画
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true" >
<item android:drawable="@drawable/pull_end_image_frame_01" android:duration="100"/> <item android:drawable="@drawable/pull_end_image_frame_02" android:duration="100"/> <item android:drawable="@drawable/pull_end_image_frame_03" android:duration="100"/> <item android:drawable="@drawable/pull_end_image_frame_04" android:duration="100"/> <item android:drawable="@drawable/pull_end_image_frame_05" android:duration="100"/> animation-list>
帧动画的启动和停止方式:
mSecondView = (MeiTuanRefreshSecondStepView) headerView.findViewById(R.id.second_view); mSecondView.setBackgroundResource(R.drawable.pull_to_refresh_second_anim); secondAnim = (AnimationDrawable) mSecondView.getBackground(); //启动 secondAnim.start(); //停止 secondAnim.stop();
第三个状态的实现:
和第二个状态同理,我们也通过自定义View来确保三个状态的View的宽高保持一致。
public class MeiTuanRefreshThirdStepView extends View{
private Bitmap endBitmap;
public MeiTuanRefreshThirdStepView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init();
}
public MeiTuanRefreshThirdStepView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MeiTuanRefreshThirdStepView(Context context) {
super(context);
init();
}
private void init() {
endBitmap = Bitmap.createBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.pull_end_image_frame_05));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), measureWidth(widthMeasureSpec)*endBitmap.getHeight()/endBitmap.getWidth());
}
private int measureWidth(int widthMeasureSpec){
int result = 0;
int size = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (mode == MeasureSpec.EXACTLY) {
result = size;
}else {
result = endBitmap.getWidth();
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
}
return result;
}
}
我们在xml中定义一组帧动画:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false" > <item android:drawable="@drawable/refreshing_image_frame_01" android:duration="100"/> <item android:drawable="@drawable/refreshing_image_frame_02" android:duration="100"/> <item android:drawable="@drawable/refreshing_image_frame_03" android:duration="100"/> <item android:drawable="@drawable/refreshing_image_frame_04" android:duration="100"/> <item android:drawable="@drawable/refreshing_image_frame_05" android:duration="100"/> <item android:drawable="@drawable/refreshing_image_frame_06" android:duration="100"/> <item android:drawable="@drawable/refreshing_image_frame_07" android:duration="100"/> <item android:drawable="@drawable/refreshing_image_frame_08" android:duration="100"/> animation-list>
下拉刷新的实现:
首先我们要定义好几个状态,下拉刷新有这样几个状态:
DONE:隐藏的状态
PULL_TO_REFRESH:下拉刷新的状态
RELEASE_TO_REFRESH:松开刷新的状态
REFRESHING:正在刷新的状态
public class MeiTuanListView extends ListView implements AbsListView.OnScrollListener{
private static final int DONE = 0;
private static final int PULL_TO_REFRESH = 1;
private static final int RELEASE_TO_REFRESH = 2;
private static final int REFRESHING = 3;
private static final int RATIO = 3;
private LinearLayout headerView;
private int headerViewHeight;
private float startY;
private float offsetY;
private TextView tv_pull_to_refresh;
private OnMeiTuanRefreshListener mOnRefreshListener;
private int state;
private int mFirstVisibleItem;
private boolean isRecord;
private boolean isEnd;
private boolean isRefreable;
private FrameLayout mAnimContainer;
private Animation animation;
private SimpleDateFormat format;
private MeiTuanRefreshFirstStepView mFirstView;
private MeiTuanRefreshSecondStepView mSecondView;
private AnimationDrawable secondAnim;
private MeiTuanRefreshThirdStepView mThirdView;
private AnimationDrawable thirdAnim;
public MeiTuanListView(Context context) {
super(context);
init(context);
}
public MeiTuanListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MeiTuanListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public interface OnMeiTuanRefreshListener{
void onRefresh();
}
/**
* 回调接口,想实现下拉刷新的listview实现此接口
* @param onRefreshListener
*/
public void setOnMeiTuanRefreshListener(OnMeiTuanRefreshListener onRefreshListener){
mOnRefreshListener = onRefreshListener;
isRefreable = true;
}
/**
* 刷新完毕,从主线程发送过来,并且改变headerView的状态和文字动画信息
*/
public void setOnRefreshComplete(){
//一定要将isEnd设置为true,以便于下次的下拉刷新
isEnd = true;
state = DONE;
changeHeaderByState(state);
}
private void init(Context context) {
setOverScrollMode(View.OVER_SCROLL_NEVER);
setOnScrollListener(this);
headerView = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.meituan_item, null, false);
mFirstView = (MeiTuanRefreshFirstStepView) headerView.findViewById(R.id.first_view);
tv_pull_to_refresh = (TextView) headerView.findViewById(R.id.tv_pull_to_refresh);
mSecondView = (MeiTuanRefreshSecondStepView) headerView.findViewById(R.id.second_view);
mSecondView.setBackgroundResource(R.drawable.pull_to_refresh_second_anim);
secondAnim = (AnimationDrawable) mSecondView.getBackground();
mThirdView = (MeiTuanRefreshThirdStepView) headerView.findViewById(R.id.third_view);
mThirdView.setBackgroundResource(R.drawable.pull_to_refresh_third_anim);
thirdAnim = (AnimationDrawable) mThirdView.getBackground();
measureView(headerView);
addHeaderView(headerView);
headerViewHeight = headerView.getMeasuredHeight();
headerView.setPadding(0, -headerViewHeight, 0, 0);
Log.i("zhangqi","headerViewHeight="+headerViewHeight);
state = DONE;
isEnd = true;
isRefreable = false;
}
@Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
@Override
public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mFirstVisibleItem = firstVisibleItem;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (isEnd) {//如果现在时结束的状态,即刷新完毕了,可以再次刷新了,在onRefreshComplete中设置
if (isRefreable) {//如果现在是可刷新状态 在setOnMeiTuanListener中设置为true
switch (ev.getAction()){
//用户按下
case MotionEvent.ACTION_DOWN:
//如果当前是在listview顶部并且没有记录y坐标
if (mFirstVisibleItem == 0 && !isRecord) {
//将isRecord置为true,说明现在已记录y坐标
isRecord = true;
//将当前y坐标赋值给startY起始y坐标
startY = ev.getY();
}
break;
//用户滑动
case MotionEvent.ACTION_MOVE:
//再次得到y坐标,用来和startY相减来计算offsetY位移值
float tempY = ev.getY();
//再起判断一下是否为listview顶部并且没有记录y坐标
if (mFirstVisibleItem == 0 && !isRecord) {
isRecord = true;
startY = tempY;
}
//如果当前状态不是正在刷新的状态,并且已经记录了y坐标
if (state!=REFRESHING && isRecord ) {
//计算y的偏移量
offsetY = tempY - startY;
//计算当前滑动的高度
float currentHeight = (-headerViewHeight+offsetY/3);
//用当前滑动的高度和头部headerView的总高度进行比 计算出当前滑动的百分比 0到1
float currentProgress = 1+currentHeight/headerViewHeight;
//如果当前百分比大于1了,将其设置为1,目的是让第一个状态的椭圆不再继续变大
if (currentProgress>=1) {
currentProgress = 1;
}
//如果当前的状态是放开刷新,并且已经记录y坐标
if (state == RELEASE_TO_REFRESH && isRecord) {
setSelection(0);
//如果当前滑动的距离小于headerView的总高度
if (-headerViewHeight+offsetY/RATIO<0) {
//将状态置为下拉刷新状态
state = PULL_TO_REFRESH;
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
//如果当前y的位移值小于0,即为headerView隐藏了
}else if (offsetY<=0) {
//将状态变为done
state = DONE;
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
}
}
//如果当前状态为下拉刷新并且已经记录y坐标
if (state == PULL_TO_REFRESH && isRecord) {
setSelection(0);
//如果下拉距离大于等于headerView的总高度
if (-headerViewHeight+offsetY/RATIO>=0) {
//将状态变为放开刷新
state = RELEASE_TO_REFRESH;
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
//如果当前y的位移值小于0,即为headerView隐藏了
}else if (offsetY<=0) {
//将状态变为done
state = DONE;
//根据状态改变headerView,主要是更新动画和文字等信息
changeHeaderByState(state);
}
}
//如果当前状态为done并且已经记录y坐标
if (state == DONE && isRecord) {
//如果位移值大于0
if (offsetY>=0) {
//将状态改为下拉刷新状态
state = PULL_TO_REFRESH;
}
}
//如果为下拉刷新状态
if (state == PULL_TO_REFRESH) {
//则改变headerView的padding来实现下拉的效果
headerView.setPadding(0,(int)(-headerViewHeight+offsetY/RATIO) ,0,0);
//给第一个状态的View设置当前进度值
mFirstView.setCurrentProgress(currentProgress);
//重画
mFirstView.postInvalidate();
}
//如果为放开刷新状态
if (state == RELEASE_TO_REFRESH) {
//改变headerView的padding值
headerView.setPadding(0,(int)(-headerViewHeight+offsetY/RATIO) ,0, 0);
//给第一个状态的View设置当前进度值
mFirstView.setCurrentProgress(currentProgress);
//重画
mFirstView.postInvalidate();
}
}
break;
//当用户手指抬起时
case MotionEvent.ACTION_UP:
//如果当前状态为下拉刷新状态
if (state == PULL_TO_REFRESH) {
//平滑的隐藏headerView
this.smoothScrollBy((int)(-headerViewHeight+offsetY/RATIO)+headerViewHeight, 500);
//根据状态改变headerView
changeHeaderByState(state);
}
//如果当前状态为放开刷新
if (state == RELEASE_TO_REFRESH) {
//平滑的滑到正好显示headerView
this.smoothScrollBy((int)(-headerViewHeight+offsetY/RATIO), 500);
//将当前状态设置为正在刷新
state = REFRESHING;
//回调接口的onRefresh方法
mOnRefreshListener.onRefresh();
//根据状态改变headerView
changeHeaderByState(state);
}
//这一套手势执行完,一定别忘了将记录y坐标的isRecord改为false,以便于下一次手势的执行
isRecord = false;
break;
}
}
}
return super.onTouchEvent(ev);
}
/**
* 根据状态改变headerView的动画和文字显示
* @param state
*/
private void changeHeaderByState(int state){
switch (state) {
case DONE://如果的隐藏的状态
//设置headerView的padding为隐藏
headerView.setPadding(0, -headerViewHeight, 0, 0);
//第一状态的view显示出来
mFirstView.setVisibility(View.VISIBLE);
//第二状态的view隐藏起来
mSecondView.setVisibility(View.GONE);
//停止第二状态的动画
secondAnim.stop();
//第三状态的view隐藏起来
mThirdView.setVisibility(View.GONE);
//停止第三状态的动画
thirdAnim.stop();
break;
case RELEASE_TO_REFRESH://当前状态为放开刷新
//文字显示为放开刷新
tv_pull_to_refresh.setText("放开刷新");
//第一状态view隐藏起来
mFirstView.setVisibility(View.GONE);
//第二状态view显示出来
mSecondView.setVisibility(View.VISIBLE);
//播放第二状态的动画
secondAnim.start();
//第三状态view隐藏起来
mThirdView.setVisibility(View.GONE);
//停止第三状态的动画
thirdAnim.stop();
break;
case PULL_TO_REFRESH://当前状态为下拉刷新
//设置文字为下拉刷新
tv_pull_to_refresh.setText("下拉刷新");
//第一状态view显示出来
mFirstView.setVisibility(View.VISIBLE);
//第二状态view隐藏起来
mSecondView.setVisibility(View.GONE);
//第二状态动画停止
secondAnim.stop();
//第三状态view隐藏起来
mThirdView.setVisibility(View.GONE);
//第三状态动画停止
thirdAnim.stop();
break;
case REFRESHING://当前状态为正在刷新
//文字设置为正在刷新
tv_pull_to_refresh.setText("正在刷新");
//第一状态view隐藏起来
mFirstView.setVisibility(View.GONE);
//第三状态view显示出来
mThirdView.setVisibility(View.VISIBLE);
//第二状态view隐藏起来
mSecondView.setVisibility(View.GONE);
//停止第二状态动画
secondAnim.stop();
//启动第三状态view
thirdAnim.start();
break;
default:
break;
}
}
private void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
}
一切准备就绪,在Activity中使用:
public class MainActivity extends Activity implements OnMeiTuanRefreshListener{
private MeiTuanListView mListView;
private List mDatas;
private ArrayAdapter mAdapter;
private final static int REFRESH_COMPLETE = 0;
/**
* mInterHandler是一个私有静态内部类继承自Handler,内部持有MainActivity的弱引用,
* 避免内存泄露
*/
private InterHandler mInterHandler = new InterHandler(this);
private static class InterHandler extends Handler{
private WeakReference mActivity;
public InterHandler(MainActivity activity){
mActivity = new WeakReference(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity 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(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (MeiTuanListView) findViewById(R.id.listview);
String[] data = new String[]{"hello world","hello world","hello world","hello world",
"hello world","hello world","hello world","hello world","hello world",
"hello world","hello world","hello world","hello world","hello world",};
mDatas = new ArrayList(Arrays.asList(data));
mAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1,mDatas);
mListView.setAdapter(mAdapter);
mListView.setOnMeiTuanRefreshListener(this);
}
@Override
public void onRefresh() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
mDatas.add(0, "new data");
mInterHandler.sendEmptyMessage(REFRESH_COMPLETE);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
}
参考别人Demo