APP实用开发—自定义加载动画
彷百度外卖动画
动画
我们先来看看Android中的动画吧:
Android中的动画分为三种:
Tween动画,这一类的动画提供了旋转、平移、缩放等效果。
Alpha – 淡入淡出
Scale – 缩放效果
Roate – 旋转效果
Translate – 平移效果
Frame动画(帧动画),这一类动画可以创建一个Drawable序列,按照指定时间间歇一个一个显示出来。
Property动画(属性动画),Android3.0之后引入出来的属性动画,它更改的是对象的实际属性。
分析
我们可以看到百度外卖的下拉刷新的头是一个骑车的快递员在路上疾行,分析一下我们得到下面的动画:
背景图片的平移动画
太阳的自旋转动画
两个小轮子的自旋转动画
这就很简单了,接下来我们去百度外面的图片资源文件里找到这几张图片:(下载百度外卖的apk直接解压即可)
定义下拉刷新头文件:headview.xml
这里注意一下:我们定义了两张背景图片的ImageView是为了可以实现背景的平移动画效果。
<?xml version="1.0" encoding="utf-8"?>
<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属性为循环往复。
<?xml version="1.0" encoding="utf-8"?>
<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%
<?xml version="1.0" encoding="utf-8"?>
<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运行时在最底层显示
<?xml version="1.0" encoding="utf-8"?>
<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、背景图片2private 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的OverScrollsetOverScrollMode(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;}@Overridepublic void onScrollStateChanged(AbsListView absListView, int i) {}@Overridepublic void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {mFirstVisibleItem = firstVisibleItem;}@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (isEnd) {//如果现在时结束的状态,即刷新完毕了,可以再次刷新了,在onRefreshComplete中设置if (isRefreable) {//如果现在是可刷新状态 在setOnMeiTuanListener中设置为trueswitch (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到1float 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) {//将状态变为donestate = 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) {//将状态变为donestate = DONE;//根据状态改变headerView,主要是更新动画和文字等信息changeHeaderByState(state);}}//如果当前状态为done并且已经记录y坐标if (state == DONE && isRecord) {//如果位移值大于0if (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) {//平滑的隐藏headerViewthis.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO)+headViewHeight, 500);//根据状态改变headerViewchangeHeaderByState(state);}//如果当前状态为放开刷新if (state == RELEASE_TO_REFRESH) {//平滑的滑到正好显示headerViewthis.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO), 500);//将当前状态设置为正在刷新state = REFRESHING;//回调接口的onRefresh方法mOnRefreshListener.onRefresh();//根据状态改变headerViewchangeHeaderByState(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*/@Overrideprotected 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*/@Overrideprotected 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);}@Overrideprotected 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));}@Overrideprotected 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定义一组帧动画
<?xml version="1.0" encoding="utf-8"?> <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));}@Overrideprotected 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中定义一组帧动画:
<?xml version="1.0" encoding="utf-8"?> <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;}@Overridepublic void onScrollStateChanged(AbsListView absListView, int i) {}@Overridepublic void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {mFirstVisibleItem = firstVisibleItem;}@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (isEnd) {//如果现在时结束的状态,即刷新完毕了,可以再次刷新了,在onRefreshComplete中设置if (isRefreable) {//如果现在是可刷新状态 在setOnMeiTuanListener中设置为trueswitch (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到1float 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) {//将状态变为donestate = 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) {//将状态变为donestate = DONE;//根据状态改变headerView,主要是更新动画和文字等信息changeHeaderByState(state);}}//如果当前状态为done并且已经记录y坐标if (state == DONE && isRecord) {//如果位移值大于0if (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) {//平滑的隐藏headerViewthis.smoothScrollBy((int)(-headerViewHeight+offsetY/RATIO)+headerViewHeight, 500);//根据状态改变headerViewchangeHeaderByState(state);}//如果当前状态为放开刷新if (state == RELEASE_TO_REFRESH) {//平滑的滑到正好显示headerViewthis.smoothScrollBy((int)(-headerViewHeight+offsetY/RATIO), 500);//将当前状态设置为正在刷新state = REFRESHING;//回调接口的onRefresh方法mOnRefreshListener.onRefresh();//根据状态改变headerViewchangeHeaderByState(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();//启动第三状态viewthirdAnim.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<String> mDatas;private ArrayAdapter<String> 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<MainActivity> mActivity;public InterHandler(MainActivity activity){mActivity = new WeakReference<MainActivity>(activity);}@Overridepublic 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);}}}@Overrideprotected 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<String>(Arrays.asList(data));mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,mDatas);mListView.setAdapter(mAdapter);mListView.setOnMeiTuanRefreshListener(this);}@Overridepublic void onRefresh() {new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(3000);mDatas.add(0, "new data");mInterHandler.sendEmptyMessage(REFRESH_COMPLETE);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}).start();}
}
参考别人Demo
APP实用开发—自定义加载动画相关推荐
- Android 自定义加载动画LoadingView
前言 本文参考辉哥的博客属性动画 - 58同城数据加载动画,用来学习属性动画相关知识非常合适. 最终效果 整体思路 绘制部分分析: 整体加载动画由三部分组成: 1.上方的正方形.圆形以及三角形,需要进 ...
- Android为网络请求自定义加载动画
android自带的加载动画都不怎么好看,在这里介绍一种自定义加载动画的方法 原始图片: 编写动画progressbar.xml, <?xml version="1.0" e ...
- 【微信小程序】自定义加载动画3
目录 效果图 配置 版本1 版本2 结语 效果图 配置 配置方法参考上一篇文章:[微信小程序]自定义加载动画 版本1 Component({behaviors
- 【微信小程序】自定义加载动画4
目录 效果图 配置文件 结语 效果图 配置文件 配置方法参考上一篇文章:[微信小程序]自定义加载动画 组件源代码: Component({behaviors: [],properties: {
- 【微信小程序】自定义加载动画
前言 在编写微信小程序的代码时,我们会发现:原生加载动画样式不多,可供我们设置的参数也不多. 通过查询可以得知,若是需要自己定义加载动画,一般的做法就是:在当前页面写加载动画的view代 ...
- echarts加载数据时自定义加载动画
echarts加载数据时自定义加载动画 1.实现 1.实现 有时用echarts加载数据时,尤其是首次加载,可能一时加载不出数据,可以给它加一个加载动画 let departChart = this. ...
- android 自定义加载动画效果,Android 自定义View修炼-自定义加载进度动画LoadingImageView...
一.概述 本自定义View,是加载进度动画的自定义View,继承于ImageView来实现,主要实现蒙层加载进度的加载进度效果. 支持水平左右加载和垂直上下加载四个方向,同时也支持自定义蒙层进度颜色. ...
- android 自定义图片加载动画效果,Android自定义加载动画-感染体
Android自定义动画系列七,今天来分享第七个自定义Loading动画(InfectionBallBuilder),看上去感觉有种病毒源被感染的感觉,所以名字就叫感染体,这个动画做出来的效果,我不怎 ...
- 【动画消消乐】HTML+CSS 自定义加载动画 068
前言 Hello!小伙伴! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出- 自我介绍 ଘ(੭ˊᵕˋ)੭ 昵称:海轰 标签:程序猿|C++选手|学生 简介:因C语言结识编程,随后转入计 ...
最新文章
- mysql 备份压缩数据库_备份压缩mysql 数据库
- 【数据库系统概论】考研第二部分重点分析【2.2】
- 操作系统实验报告10:线程1
- oracle sql 表个数,【oracleSQL】查看当前用户各个表的记录数
- Linux之文件通信
- 如何开始使用Java中的Lambda表达式
- 这些C4D创意卡通色彩场景,简直酷到爆炸!
- Html5中的徽章,CSS 常见样式 特殊用法 贯穿线徽章箭头
- 使用Xunit来进行单元测试
- 美国住宅保修公司新增支持加密货币支付,并决定将 BTC 列入资产负债表
- RGB与YUV格式(四)
- cvEqualizeHist() 直方图均衡化(单通道)
- ubuntu 关机后自动重启(并没有解决,服了)
- 【vue-router源码】四、createRouter源码解析
- 2020年6月六级作文和翻译三国演义
- php示例代码之使用MySQLi接口
- 将示波器信号完整数据导入Matlab进行分析
- java的移位操作详解,左移和右移
- 用Selenium+xpath爬取京东商城
- 多卡聚合路由器5G+4G是什么意思
热门文章
- 游戏化思维——核心驱动力
- GDR(Gradual Decoder Refresh)帧
- 多元微积分_stokes定理证明
- c++二分法求平方根
- python柱形图绘制_Python数据分析:柱形图的绘制方法
- 从公司角度来看,为什么要招实习生?
- 正则html在线测试,正则表达式在线测试工具
- 计算机网络网卡作用是什么,什么是网卡?它的作用是什么?
- Linux TCP/IP大合集
- 复现SCRDet:Towards More Robust Detection for Small, Cluttered and Rotated Objects(ICCV2019)遇到的问题及解决方案