Android实战页面内容加载动画
2015-06-18 19:57
405 查看
前言
文章开头来看一下本篇文章要实现的效果,如图左边是慕课网APP中的效果,右边是58同城APP中的加载动画。
实现第一种动画
之前用图形的混合模式和贝塞尔曲线实现过慕课网的下拉刷新的加载动画。见链接慕课网app下拉刷新图标填充效果的实现,而这种动画效果在app中其实也很常见,之前的那篇文章是自定义View绘制出来的,其实这个也可以用DrawableAnimation实现,这里,我们来实现一下,看看有多简单。首先提取图片资源,图片提取自慕课网App,如图。提取完图片之后就是编写Drawable文件
<?xml version="1.0" encoding="utf-8"?> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@mipmap/head_image_default" android:duration="50"/> <item android:drawable="@mipmap/head_image_0" android:duration="50"/> <item android:drawable="@mipmap/head_image_1" android:duration="50"/> <item android:drawable="@mipmap/head_image_2" android:duration="50"/> <item android:drawable="@mipmap/head_image_3" android:duration="50"/> <item android:drawable="@mipmap/head_image_4" android:duration="50"/> <item android:drawable="@mipmap/head_image_5" android:duration="50"/> <item android:drawable="@mipmap/head_image_6" android:duration="50"/> <item android:drawable="@mipmap/head_image_7" android:duration="50"/> <item android:drawable="@mipmap/head_image_8" android:duration="50"/> <item android:drawable="@mipmap/head_image_9" android:duration="50"/> <item android:drawable="@mipmap/head_image_10" android:duration="50"/> <item android:drawable="@mipmap/head_image_11" android:duration="50"/> <item android:drawable="@mipmap/head_image_12" android:duration="50"/> <item android:drawable="@mipmap/head_image_13" android:duration="50"/> <item android:drawable="@mipmap/head_image_14" android:duration="50"/> <item android:drawable="@mipmap/head_image_15" android:duration="50"/> <item android:drawable="@mipmap/head_image_16" android:duration="50"/> <item android:drawable="@mipmap/head_image_17" android:duration="50"/> <item android:drawable="@mipmap/head_image_18" android:duration="50"/> <item android:drawable="@mipmap/head_image_19" android:duration="50"/> <item android:drawable="@mipmap/head_image_20" android:duration="50"/> <item android:drawable="@mipmap/head_image_21" android:duration="50"/> <item android:drawable="@mipmap/head_image_22" android:duration="50"/> <item android:drawable="@mipmap/head_image_23" android:duration="50"/> <item android:drawable="@mipmap/head_image_24" android:duration="50"/> <item android:drawable="@mipmap/head_image_25" android:duration="50"/> <item android:drawable="@mipmap/head_image_26" android:duration="50"/> <item android:drawable="@mipmap/head_image_27" android:duration="50"/> <item android:drawable="@mipmap/head_image_28" android:duration="50"/> <item android:drawable="@mipmap/head_image_29" android:duration="50"/> <item android:drawable="@mipmap/head_image_30" android:duration="50"/> </animation-list>
为了复用这个控件,我们选中新建一个MoocView类继承ImageView,在构造方法中设置背景图为我们的drawable文件,设置完后拿到background强转为AnimationDrawable,通过调用AnimationDrawable对象的start方法开始动画,stop方法停止动画,有时候一开始我们的View没有显示,而当设置了View.VISIBLE后,此时动画应该立即执行,因此,我们重写setVisible方法,当设置为View.VISIBLE时开始动画,否则结束动画
package cn.edu.zafu.drawableanimation; import android.content.Context; import android.graphics.drawable.AnimationDrawable; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; /** * User: lizhangqu(513163535@qq.com) * Date: 2015-06-18 * Time: 15:57 * 慕课网下拉刷新进度显示控件 */ public class MoocView extends ImageView{ private AnimationDrawable background; public MoocView(Context context) { this(context, null); } public MoocView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MoocView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { setBackgroundResource(R.drawable.refresh_anim); background= (AnimationDrawable) getBackground(); } public void startAnimator(){ if(background!=null){ background.start(); } } public void stopAnimator(){ if(background!=null){ background.stop(); } } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); if(visibility== View.VISIBLE){ startAnimator(); }else{ stopAnimator(); } } }
写完了这些直接在布局文件里使用即可。
实现第二种动画
相对前面的动画,第二种动画就显得有点复杂了,第二种动画使用属性动画的相关类完成。如果要向下兼容,请使用兼容库NineOldAndroids,使用方法和元素的属性动画基本一致。分析
在正式编码前,我们来分析一下第二种动画,首先动画可以分为四部分,圆形,正方形,三角形,底部阴影四部分,其中底部阴影动画是水平方向的缩放动画,它跟其他三个的动画同时进行,但是另外三个的动画是一个接一个执行的,并且每一个图形的动画都涉及到多种动画,比如垂直方向的平移,以及旋转。然后垂直方向从下向上运动,再从上向下运动,运动到远处的时候变换图形,而这一过程的速度变换应该是垂直方向的抛体运动,即先变小再变大。而系统并没有这个插值器,所以这个插值器也需要自己编写。那么再说另一个问题,如何实现图形的切换,其实很简单,刚开始的时候将另外两个图形设置为不可见,只显示第一个图形,监听动画,当第一个图形的动画完成后,将其隐藏,将接下来的一个图形设置为可见,同样监听这个动画,依次类推。实现插值器
关于插值器的知识,这里不做解释,见这篇文章Android中的Interpolator我们只要继承Interpolator接口,实现相应的方法即可。而当务之急就是找出这么一个曲线,其斜率变换是先变小再变大的,即只要构造出如图所示的曲线,当x小于0.5的时候,取曲线b,当x大于等于0.5的时候取曲线a,曲线不唯一,自己取一条即可
package cn.edu.zafu.drawableanimation; import android.view.animation.Interpolator; public class DecelerateAccelerateInterpolator implements Interpolator { private float mFactor = 1.0f; public DecelerateAccelerateInterpolator() { } public DecelerateAccelerateInterpolator(float factor) { mFactor = factor; } public float getInterpolation(float input) { float result; if (input < 0.5) { result = (float) (1.0f - Math.pow((1.0f - 2 * input), 2 * mFactor)) / 2; } else { result = (float) Math.pow((input - 0.5) * 2, 2 * mFactor) / 2 + 0.5f; } return result; } }
编写自定义View
新建一个LoadingView 类,继承RelativeLayoutpackage cn.edu.zafu.drawableanimation; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.ImageView; import android.widget.RelativeLayout; /** * User: lizhangqu(513163535@qq.com) * Date: 2015-06-18 * Time: 14:08 * 58同城页面加载动画View */ public class LoadingView extends RelativeLayout { private Context mContext; public LoadingView(Context context) { this(context, null); } public LoadingView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; } private int dip2px(int dip) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics()); } }
声明内部的View的相关变量
private static final int DEFAULT_VIEW_SIZE = 28; private static final int DURATION = 800; private static final int TOP_HEIGHT=80; private static final int RATATION_HEIGHT=20; private int mViewSize; private ImageView mCircleView; private ImageView mRectView; private ImageView mTriangleView; private ImageView mBottomView;
声明动画相关的成员变量
private AnimatorSet mAll; private AnimatorSet mCircleAnimator; private AnimatorSet mRectAnimator; private AnimatorSet mTriangleAnimator; private Animator.AnimatorListener mCircleListener; private Animator.AnimatorListener mRectListener; private Animator.AnimatorListener mTriangleListener; private boolean isAnimator = false; private final float[] TRANSLATIONY ={0f, -dip2px(TOP_HEIGHT), 0f}; private final float[] SCALEX = {0.9f, 0.5f, 0.2f, 0.1f, 0.05f, 0.1f, 0.2f, 0.3f, 0.5f, 0.7f, 0.9f}; private final float[] ROTATION_RECT ={0f, 200f}; private final float[] ROTATION_TRIANGLE = {0f, -90f};
对View进行初始化
private void initView() { mViewSize = dip2px(DEFAULT_VIEW_SIZE); setGravity(Gravity.CENTER); mCircleView = new ImageView(mContext); mCircleView.setId(R.id.top); mCircleView.setBackgroundResource(R.mipmap.loading_yuan); LayoutParams circleParams = new LayoutParams(mViewSize, mViewSize); circleParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE); circleParams.topMargin=dip2px(TOP_HEIGHT+RATATION_HEIGHT); mCircleView.setLayoutParams(circleParams); addView(mCircleView); mRectView = new ImageView(mContext); mRectView.setPivotX(mViewSize/2); mRectView.setPivotY(mViewSize/2); mRectView.setBackgroundResource(R.mipmap.loading_fangxing); LayoutParams rectParams = new LayoutParams(mViewSize, mViewSize); rectParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE); rectParams.topMargin=dip2px(TOP_HEIGHT+RATATION_HEIGHT); mRectView.setLayoutParams(rectParams); addView(mRectView); mTriangleView = new ImageView(mContext); mTriangleView.setPivotY(mViewSize/2); mTriangleView.setPivotX(mViewSize/2); mTriangleView.setBackgroundResource(R.mipmap.loading_sanjiao); LayoutParams triangleParams = new LayoutParams(mViewSize, mViewSize); triangleParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE); triangleParams.topMargin=dip2px(TOP_HEIGHT+RATATION_HEIGHT); mTriangleView.setLayoutParams(triangleParams); addView(mTriangleView); mBottomView = new ImageView(mContext); mBottomView.setBackgroundResource(R.mipmap.loading_bottom); LayoutParams bottomParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); bottomParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE); bottomParams.addRule(RelativeLayout.BELOW,R.id.top); mBottomView.setLayoutParams(bottomParams); addView(mBottomView); mRectView.setVisibility(View.INVISIBLE); mTriangleView.setVisibility(View.INVISIBLE); }
初始化跟动画相关的监听器
private void initAnimatorListener() { mCircleListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mTriangleView.setVisibility(View.INVISIBLE); mCircleView.setVisibility(View.INVISIBLE); mRectView.setVisibility(View.VISIBLE); } }; mRectListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCircleView.setVisibility(View.INVISIBLE); mTriangleView.setVisibility(View.VISIBLE); mRectView.setVisibility(View.INVISIBLE); } }; mTriangleListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCircleView.setVisibility(View.VISIBLE); mRectView.setVisibility(View.INVISIBLE); mTriangleView.setVisibility(View.INVISIBLE); isAnimator = false; startAnimator(); } }; }
编写获得动画相关类的几个函数,主要是为了复用代码
private Animator getBottomViewAnimator() { ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mBottomView, "scaleX", SCALEX); objectAnimator.setInterpolator(new DecelerateAccelerateInterpolator()); return objectAnimator; } private Animator getTranslationAnimator(Object object) { ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(object, "translationY", TRANSLATIONY); objectAnimator.setInterpolator(new DecelerateAccelerateInterpolator()); return objectAnimator; } private Animator getRotationAnimator(Object object,float[] values) { ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(object, "rotation", values); objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); return objectAnimator; }
编写开始动画的函数,其逻辑前面已经介绍了,关键是三个图形的动画有一个延时,而底部的动画与三个图形的动画一起进行,如果当前正在进行动画,则直接return,在动画执行前记得设置监听器。
public void startAnimator() { if(isAnimator){ return; } isAnimator = true; mCircleAnimator = new AnimatorSet(); mCircleAnimator.setDuration(DURATION); mCircleAnimator.playTogether(getTranslationAnimator(mCircleView), getBottomViewAnimator()); mCircleAnimator.addListener(mCircleListener); mRectAnimator = new AnimatorSet(); mRectAnimator.setDuration(DURATION); mRectAnimator.setStartDelay(DURATION); mRectAnimator.playTogether(getTranslationAnimator(mRectView), getBottomViewAnimator(), getRotationAnimator(mRectView, ROTATION_RECT)); mRectAnimator.addListener(mRectListener); mTriangleAnimator = new AnimatorSet(); mTriangleAnimator.setDuration(DURATION); mTriangleAnimator.setStartDelay(DURATION * 2); mTriangleAnimator.playTogether(getTranslationAnimator(mTriangleView), getBottomViewAnimator(), getRotationAnimator(mTriangleView, ROTATION_TRIANGLE)); mTriangleAnimator.addListener(mTriangleListener); mCircleAnimator.start(); mRectAnimator.start(); mTriangleAnimator.start(); }
既然有开始动画也就有结束动画的函数,结束动画有个缓冲过程,即执行完当前正在执行的动画后才结束,该函数需要将各个动画停止,让后将监听器移除,并设置成员变量isAnimator为false
public void stopAnimator() { if (mCircleAnimator != null) { mCircleAnimator.end(); mCircleAnimator.removeAllListeners(); mCircleAnimator = null; } if (mRectAnimator != null) { mRectAnimator.end(); mRectAnimator.removeAllListeners(); mRectAnimator = null; } if (mTriangleAnimator != null) { mTriangleAnimator.end(); mTriangleAnimator.removeAllListeners(); mTriangleAnimator = null; } isAnimator = false; }
重新setVisible方法,正如之前所说的,图形可能刚开始时不可见的,后来可以通过setVisible方法使其可见,可见的同时动画也应该开始执行,这时候重写setVisible方法实现相应的逻辑即可
@Override public void setVisibility(int visibility) { super.setVisibility(visibility); if (visibility == View.VISIBLE) { if (!isAnimator) { startAnimator(); } } else { if (isAnimator) { stopAnimator(); } } }
题外话
关于属性动画可以参见郭霖的三篇文章,个人觉得讲得很详细。Android属性动画完全解析(上),初识属性动画的基本用法
Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
而在使用属性动画的时候,可能有时候会遇到修改不具有getter/setter方法的属性,此时我们可以通过一层包装,类似于适配器模式,给它提供一个getter和setter方法,如下所示
public class WrapperView { private View mTarget; public WrapperView(View target) { mTarget = target; } public int getWidth() { return mTarget.getLayoutParams().width; } public void setWidth(int width) { mTarget.getLayoutParams().width = width; mTarget.requestLayout(); } }
使用也很简单
ViewWrapper wrapper = new ViewWrapper(mButton); ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start();
源码下载
http://download.csdn.net/detail/sbsujjbcy/8820513相关文章推荐
- 【进阶android】ListView源码分析——适配器及观察者模式
- 关于android.view.WindowLeaked异常的解决方案
- Android PinnedSectionListView 收缩
- Android系统中Parcelable和Serializable的区别
- Android getWindow().setFlags(。。。。。。)
- Android 框架练成 教你打造高效的图片加载框架
- Android中和javascript交互报:android Uncaught ReferenceError:
- Android之SharedPreferences工具类
- android studio 操作指南
- Android开发框架AndBase
- Android对话框的两种创建方式
- vs2015 支持Android arm neon Introducing Visual Studio’s Emulator for Android
- HandlerThread 的使用
- android viewpager outofmemery问题解决
- android之view的TouchDelegate
- Android-只有2个圆角的ImageView
- 【Android开发】多媒体应用开发-使用VideoView播放视频
- Android-用户提醒Notification
- 【Android开发】多媒体应用开发-使用SoundPool播放音频
- Android复制WIN8点击下沉倾斜系统瓷砖效果