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

1. 说明


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


图片.png

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 2185134304@qq.com
 * 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——布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:gravity="center"
    >


    <!-- 最上边可变化的形状 -->
    <com.jackchen.day_21.ShapeView
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_marginBottom="82dp"
        android:id="@+id/shape_view"
        />

    <View
        android:layout_width="25dp"
        android:layout_height="3dp"
        android:background="@drawable/loading_shadow_bg"
        android:id="@+id/shadow_view"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="玩命加载中..."
        android:layout_marginTop="5dp"
        />
</LinearLayout>

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.jackchen.day_21.MainActivity">

    <com.jackchen.day_21.LoadingView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/loading_view"
        >


    </com.jackchen.day_21.LoadingView>

</RelativeLayout>

就是这样,代码拷贝进去可直接使用,这里在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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容