属性动画 - 58同城数据加载动画

1. 说明


我们前边讲解了MD之后,那么从这节课开始我们就开始我们新的内容 - 属性动画。那么关于动画我在前边已经讲解了我们常用的一些动画,不太清楚的可以看下。那么我们今天所要讲解的一个效果其实很简单,就是一个早期的58同城数据加载动画,一般会用于我们加载一个ListView、RecyclerView的列表数据之前的一个动画,当数据还没加载成功时候显示动画,当数据加载成功之后隐藏动画,效果如下:


属性动画 - 58同城数据加载动画_第1张图片
图片.png

2. 分析


那么我们现在就来分析下,如何实现这个效果,其实只要分析好了过程,也没有那么难。分解图片如下:


属性动画 - 58同城数据加载动画_第2张图片
58同城数据加载动画效果分析.png

2.1 加载显示组合控件,即就是一个布局文件;

布局文件分为3个部分 = ShapeView + View(阴影背景 shape = "oval" 表示椭圆) + TextView(固定文字)
自定义一个 LoadingView,将这个布局文件加载到自定义View中,动态添加 布局文件到 自定义View中有2种方式;

第一种:
   View  loadView = inflate(getContext(),R.layout.ui_loading_view ,null);
   addView(loadView) ;
第二种:
   inflate(getContext(),R.layout.ui_loading_view ,this);
   此处的this就表示 把ui_loading_view加载到 LoadingView中

2.2 分析实现动画;
2.3 实现形状切换的view;

千万不要直接把ui_loading_view布局文件直接使用include引入到要加载数据的页面,因为这样的话你需要去findViewById()去找到很多控件然后也需要写很多代码,工作量很大,所以在这里我们需要自定义 View,在需要使用的时候我们去用就行。

3. 代码如下

3.1 ShapeView.class —— 3个动画不断切换代码如下:

/**
 * Email [email protected]
 * Created by JackChen on 2018/1/26.
 * Version 1.0
 * Description:  3个形状不断切换
 */
public class ShapeView extends View {
    private Shape mCurrentShape = Shape.Circle;
    Paint mPaint;
    private Path mPath;

    public ShapeView(Context context) {
        this(context, null);
    }

    public ShapeView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ShapeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 只保证是正方形
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(Math.min(width, height), Math.min(width, height));
    }

    @Override
    protected void onDraw(Canvas canvas) {

        switch (mCurrentShape) {
            case Circle:
                // 画圆形
                int center = getWidth() / 2;
                mPaint.setColor(ContextCompat.getColor(getContext(),R.color.circle));
                canvas.drawCircle(center, center, center, mPaint);
                break;
            case Square:
                // 画正方形
                mPaint.setColor(ContextCompat.getColor(getContext(),R.color.rect));
                canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
                break;
            case Triangle:
                // 画三角  Path 画路线
                mPaint.setColor(ContextCompat.getColor(getContext(),R.color.triangle));
                if (mPath == null) {
                    // 画路径
                    mPath = new Path();
                    mPath.moveTo(getWidth() / 2, 0);
                    mPath.lineTo(0, (float) ((getWidth()/2)*Math.sqrt(3)));
                    mPath.lineTo(getWidth(), (float) ((getWidth()/2)*Math.sqrt(3)));
                    // path.lineTo(getWidth()/2,0);
                    mPath.close();// 把路径闭合
                }
                canvas.drawPath(mPath, mPaint);
                break;
        }
    }

    /**
     * 改变形状
     */
    public void exchange() {
        switch (mCurrentShape) {
            case Circle:
                mCurrentShape = Shape.Square;
                break;
            case Square:
                mCurrentShape = Shape.Triangle;
                break;
            case Triangle:
                // 画三角  Path 画路线
                mCurrentShape = Shape.Circle;
                break;
        }
        // 不断重新绘制形状 如果是圆就绘制成正方形;如果是正方形就绘制成三角;如果是三角形就绘制成圆
        invalidate();
    }

    public enum Shape {
        Circle, Square, Triangle
    }

    public Shape getCurrentShape() {
        return mCurrentShape;
    }
}

3.2 LoadingView.class ——仿58同城数据加载动画,代码如下:

/**
 * Created by JackChen on 2018/3/7.
 * Decription : 仿58同城数据加载动画
 */

public class LoadingView extends LinearLayout {

    // 上边的形状
    private ShapeView mShapeView;
    // 中间的阴影
    private View mShadowView;
    // 下落的高度
    private int mTranslationDistance ;
    // 动画执行的时间
    private final long ANIMATOR_DURATION = 350;
    // 是否停止动画
    private boolean mIsStopAnimator = false;


    public LoadingView(Context context) {
        this(context,null);
    }

    public LoadingView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mTranslationDistance = dp2px(80) ;
        // 初始化加载布局
        initLayout() ;
    }


    /**
     * dp - px
     * @param dip
     * @return
     */
    private int dp2px(int dip) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP , dip , getResources().getDisplayMetrics());
    }


    /**
     * 初始化加载布局
     */
    private void initLayout() {
        // 1. 加载写好的 ui_loading_view
        // 1.1 实例化view
//        View loadView = inflate(getContext() , R.layout.ui_loading_view , null) ;
//        // 1.2 添加到该View中
//        addView(loadView);


        // 找一下 插件式换肤资源加载那一节内容
        // this就代表把 ui_loading_view 加载到 LoadingView中
        inflate(getContext() , R.layout.ui_loading_view , this) ;

        mShapeView = (ShapeView) findViewById(R.id.shape_view);
        mShadowView = (View) findViewById(R.id.shadow_view);

        post(new Runnable() {
            @Override
            public void run() {
                // onResume()之后 view绘制流程执行完毕之后  开始动画
                startFallAnimation() ;
            }
        }) ;
    }


    /**
     * 开始下落动画
     */
    private void startFallAnimation() {
        // 如果动画正在进行,就让其停止
        if (mIsStopAnimator){
            return;
        }
        // 下落的位移动画  translationY表示位移动画 Y方向
        // 参数一:动画作用在谁身上 参数二:动画名称 参数三:起点位置 参数四:高度
        ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mShapeView , "translationY", 0 , mTranslationDistance) ;
        // 配合中间阴影缩小 scaleX表示缩放动画 X方向
        ObjectAnimator scaleAnimatator = ObjectAnimator.ofFloat(mShadowView , "scaleX" , 1f , 0.3f) ;


        AnimatorSet animatorSet = new AnimatorSet();
        // 时长
        animatorSet.setDuration(ANIMATOR_DURATION);
        // 下落的速度应该是越来越快
        animatorSet.setInterpolator(new AccelerateInterpolator());
        // 多个动画一起执行
        animatorSet.playTogether(translationAnimator , scaleAnimatator);
        // 按顺序执行 先执行translationAnimator 后执行scaleAnimatator
//        animatorSet.playSequentially(translationAnimator , scaleAnimatator);

        animatorSet.start();

        // 下落完之后就开始上抛,监听动画执行完毕
        animatorSet.addListener(new AnimatorListenerAdapter() {

            // 动画执行完之后
            @Override
            public void onAnimationEnd(Animator animation) {
//                super.onAnimationEnd(animation);
                // 改变形状
                mShapeView.exchange();
                // 下落完之后就开始上抛了
                startUpAnimator() ;
            }
        });
    }


    /**
     *  开始执行上抛动画
     */
    private void startUpAnimator() {
        // 如果动画正在进行,就让其停止
        if(mIsStopAnimator){
            return;
        }

        // 下落的位移动画 translationY 位移动画 Y方向
        //上抛动画
        ObjectAnimator transtationAnimator = ObjectAnimator.ofFloat(mShapeView , "translationY" , mTranslationDistance , 0) ;
        // 配合中间阴影放大
        ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(mShadowView , "scaleX" , 0.3f , 1f) ;

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(ANIMATOR_DURATION) ;
        // 上抛的时候越来越慢
        animatorSet.setInterpolator(new DecelerateInterpolator()); //差值器
        animatorSet.playTogether(transtationAnimator , scaleAnimator);

        // 上抛完之后就开始下落,监听动画执行完毕
        animatorSet.addListener(new AnimatorListenerAdapter() {

            // 动画执行完毕
            @Override
            public void onAnimationEnd(Animator animation) {
                // 上抛完之后就开始下落了
                startFallAnimation();
            }

            // 动画开始执行
            @Override
            public void onAnimationStart(Animator animation) {
                // 开始旋转
                startRotationAnimator() ;

            }
        });

        // 开始动画
        animatorSet.start();
    }

    private void startRotationAnimator() {
        ObjectAnimator rotationAnimator = null ;
        switch (mShapeView.getCurrentShape()){
            case Circle:
            case Square:
                // 180
                rotationAnimator = ObjectAnimator.ofFloat(mShapeView,"rotation",0,180);
                break;
            case Triangle:
                // 120
                rotationAnimator = ObjectAnimator.ofFloat(mShapeView,"rotation",0,-120);
                break;
        }

        rotationAnimator.setDuration(ANIMATOR_DURATION) ;
        rotationAnimator.setInterpolator(new DecelerateInterpolator());
        rotationAnimator.start();

    }



    // 设置控件的隐藏
    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(View.INVISIBLE);    //不要再去摆放和计算,少走一些系统的源码, 参考View的绘制流程
        // 清理动画
        mShapeView.clearAnimation();
        mShadowView.clearAnimation();
        // 把LoadingView从父布局中移除
        ViewGroup parent = (ViewGroup) getParent();
        if (parent != null){
            parent.removeView(this);
            removeAllViews();
        }
        mIsStopAnimator=true;

    }
}

3.3 ui_loading_view.xml——布局文件如下:





    
    

    

    

具体使用,可直接在activity_main.xml文件中直接引用 LoadingView即可,activity_main.xml布局文件如下:




    


    



就是这样,代码拷贝进去可直接使用,这里在MainActivity中测试,让其显示3秒,3秒后可让其隐藏,然后让自己对应的 ListView或RecyclerView的数据显示即可;

public class MainActivity extends AppCompatActivity {

    private LoadingView loading_view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        loading_view = (LoadingView) findViewById(R.id.loading_view);


        // 3秒之后让动画隐藏
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                loading_view.setVisibility(View.GONE);
            }
        } , 3000) ;
    }
}

4. 注意


在这里一定要注意:
在自定义View的LoadingView中的最后这段代码:

    /**
     * 针对于动画涉及到内存泄露的处理情况
     * @param visibility
     */
    @Override
    public void setVisibility(int visibility) {
        // 不要再去摆放和计算,少走一些系统源码
        super.setVisibility(View.INVISIBLE);

        // 清理动画
        mLeftView.clearAnimation();
        mMiddleView.clearAnimation();
        mRightView.clearAnimation();

        // 把LoadingView从父布局中移除
        ViewGroup parent = (ViewGroup) getParent();
        if (parent != null){
            parent.removeView(this);
            removeAllViews();
        }

    }

这段代码意思就是 :
凡是动画,都可能会涉及到内存泄露,这个时候采用这种方式去清除动画,可以解决内存泄露问题,总共分为3步:
4.1 首先清除所有的子View的动画,这里有3个子View,分别为mLeftView、mMiddleView、mRightView;
4.2 获取父布局,并判断如果父布局如果不为空,首先移除父布局中的子View,最后移除所有的View即可。

这里的处理和花束直播数据加载效果 处理方式是一样的

具体代码已上传至github:
https://github.com/shuai999/View_day21.git

你可能感兴趣的:(属性动画 - 58同城数据加载动画)