Android动效loading android动效开发_i++

作者:laer_L

很久没写东西了,又来一下,可能看了这个东效你会觉得很简单,但是如果你看完,我相信你还是会发现简单里面还是有文章,也许并不是看起来那样简单,本文精华在于 性能

老规矩还是先上GIF

Android动效loading android动效开发_i++_02

也许看到这个图,你就不想再继续看下去了,心想这个动画很简单啊,不就是创建循环创建view,再给每个view的动画,每个view的开始运动的方向随机,再给一个加速器就搞定了嘛,如果你也是这样想那就该把这个文章看完了
分析:

  • 首先创建水滴动画、缩放伴随透明度变化
  • 消失时缩放伴随移动
  • 水滴展示中是一直上下浮动的
  • 每个水滴上下浮动的方向不定
  • 每个水滴运动的速度时而快时而慢(这点也许你看不出,所以我再把
    抖动的范围加大再来一个GIF)

Android动效loading android动效开发_i++_03

首先我们肯定不能用每个view对应一个动画来处理,因为如果我是100低水滴,那岂不是要100个动画,这不得卡死呀,所以肯定是一个动画来完成,开始我第一想到的也是用ValueAnimator来做,但是一个ValueAnimator怎么去控制每个view的运动方向呢,有可能你会说每个view在初始化的时候给一个反向,确实可以解决运动方向不同的问题,但是怎么解决view运动的快慢不一样,并且时而快时而慢呢,并且每个view的运动规律根本不一样,最后我选择了handler来处理,但是到这里你以为完了嘛,继续往下看吧

正题

1 . 首先创建view

1、给view一个随机的方向并且保存到view的tag里
            //随机设置view动画的方向
            view.setTag(R.string.isUp, mRandom.nextBoolean());
 2、随机设置view的位置(我这里并非完全随机,而是给了一些值,然后随机选择这些值)、这里用了一个新的集合保存已经选择到的数,下次选择的时候排除这些值,因为最好水滴不要完全重合嘛。
/**但是其实这不是我最终的方法,先往下看吧,还有彩蛋**/
 /**
     * 获取x轴或是y轴上的随机值
     *
     * @return
     */
    private double getX_YRandom(List<Float> choseRandoms,List<Float> saveRandoms) {
        float random = 0;
        while (random == 0 || saveRandoms.contains(random)) {
            random = choseRandoms.get(mRandom.nextInt(choseRandoms.size()));
        }

        saveRandoms.add(random);
        return random;
    }

还有一个动画显示view,这个就不展示了,绝对都能搞定

2 . 接下来为view设置一个初始的随机加速度(其实也是随机在已有的值中选取,因为速度不能相差太大)

/**控制水滴动画的快慢*/
    private List<Float> mSpds = Arrays.asList(2.0f, 1.7f, 1.5f, 1.3f);
 /**
     * 设置所有子view的加速度
     */
    private void setViewsSpd() {
        for (int i = 0; i < mViews.size(); i++) {
            View view = mViews.get(i);
            setSpd(view);
        }
    }
 /**
     * 设置所有子view的加速度
     */
    private void setViewsSpd() {
        for (int i = 0; i < mViews.size(); i++) {
            View view = mViews.get(i);
            setSpd(view);
        }
    }

3 . 接下来就是使用handler设置view的偏移量了,这部分也是很关键的

/**
     * 设置偏移
     */
    private void setOffSet() {
        for (int i = 0; i < mViews.size(); i++) {
            View view = mViews.get(i);
            float spd = (float) view.getTag(R.string.spd);
            float original = (float) view.getTag(R.string.original_y);
            float step = CHANGE_RANGE / BASE_OFFSET_MUL * spd;
            //拿到初始设置的方向
            boolean isUp = (boolean) view.getTag(R.string.isUp);
            float translationY;
            if (isUp) {
                translationY = view.getY() - step;
            } else {
                translationY = view.getY() + step;
            }
            //控制view的移动范围不能大于我们规定的范围,如果到了这个值还要改变view的运动方向
            if (translationY - original > CHANGE_RANGE) {
                translationY = original + CHANGE_RANGE;
                view.setTag(R.string.isUp, true);
            } else if (translationY - original < -CHANGE_RANGE) {
                translationY = original - CHANGE_RANGE;
//每次view运动到最下边,再一次向上运动时改变view的加速度,这样不就做到了view时而快时而慢了么
                setSpd(view);
                view.setTag(R.string.isUp, false);
            }
            view.setY(translationY);
        }
    }

4 . 接下来水滴点击后的消失动画,也不说了,都行

到这里动效就完了,但是我的工作并没完,打开profiler一看OMG,在初始化view的地方内存剧增,数量稍稍多一点(10个)还会卡主,看来还的优化啊

Android动效loading android动效开发_List_04

很明显private double getX_YRandom(List choseRandoms, List saveRandoms)这个方法走了太多次,原因就在于我是循环创建view,并且在这个循环内为view随机创建位置,但是为了不完全重合,我这里又一次循环知道是一个不同的值为止,也就是说这里双重循环了

  • 优化随机取用一个值后,就把这个值从集合移除,这样不就不会取到一样的值了么,到这里就真的完了,优化后实测200个都没有一点卡顿,读者可以根据自己需求优化水滴的位置逻辑算法,因为我们产品明确说了最多6滴,所以我现在的水滴位置计算逻辑足够了,还是来个GIF吧

Android动效loading android动效开发_i++_05

全部代码

/**
 * 创建时间: 2018/1/9
 * 创建人:  laitianbing
 * 描述:  蚂蚁森林模拟
 */

public class WaterView extends FrameLayout {
    private static final int WHAT_ADD_PROGRESS = 1;
    /**用来计算偏差值基础倍数,值越大,偏移越小*/
    private static final int BASE_OFFSET_MUL = 12;
    /**view变化的y抖动范围*/
    private static final int CHANGE_RANGE = 50;
    /**控制抖动动画执行的快慢*/
    public static final int PROGRESS_DELAY_MILLIS = 60;
    /**控制移除view的动画执行时间*/
    public static final int REMOVE_DELAY_MILLIS = 2000;
    /**添加水滴时动画显示view执行的时间*/
    public static final int ANIMATION_SHOW_VIEW_DURATION = 500;
    /**控制水滴动画的快慢*/
    private List<Float> mSpds = Arrays.asList(2.0f, 1.7f, 1.5f, 1.3f);
    /**x最多可选取的随机数值*/
    private static final List<Float> X_MAX_CHOSE_RANDOMS = Arrays.asList(
            0.01f,0.05f,0.1f,0.6f,0.11f, 0.16f, 0.21f, 0.26f, 0.31f, 0.7f, 0.75f, 0.8f, 0.85f, 0.87f);
    /**y最多可选取的随机数值*/
    private static final List<Float> Y_MAX_CHOSE_RANDOMS = Arrays.asList(
            0.01f,0.06f,0.11f, 0.17f, 0.23f, 0.29f, 0.35f, 0.41f, 0.47f, 0.53f, 0.59f, 0.65f, 0.71f, 0.77f, 0.83f);
    /**x坐标当前可选的随机数组*/
    private List<Float> mXCurrentCanShoseRandoms = new ArrayList<>();
    /**y坐标当前可选的随机数组*/
    private List<Float> mYCurrentCanShoseRandoms = new ArrayList<>();

    /**已经选取x的随机数值*/
    private List<Float> mXRandoms = new ArrayList<>();
    /**已经选取y的随机数值*/
    private List<Float> mYRandoms = new ArrayList<>();


    private Random mRandom = new Random();
    private List<View> mViews = new ArrayList<>();
    private int mChildViewRes = R.layout.water_item;//子view的资源文件

    private LayoutInflater mInflater;
    private int mTotalConsumeWater;//总的已经点击的水滴
    private boolean isOpenAnimtion;//是否开启动画
    private boolean isCancelAnimtion;//是否销毁动画
    private int maxX, maxY;//子view的x坐标和y坐标的最大取值
    private float mMaxSpace;//父控件对角线的距离
    private Point mDestroyPoint ;//view销毁时的点


    public WaterView(@NonNull Context context) {
        this(context, null);
    }

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

    public WaterView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mInflater = LayoutInflater.from(getContext());
    }

    @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (isCancelAnimtion) {
                return;
            }
            setOffSet();
            mHandler.sendEmptyMessageDelayed(WHAT_ADD_PROGRESS, PROGRESS_DELAY_MILLIS);
        }
    };

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mMaxSpace = (float) Math.sqrt(w * w + h * h);
        mDestroyPoint=new Point((int) getX(), h);
        maxX = w;
        maxY = h;
    }

    /**
     * 重置子view
     */
    private void reset() {
        isCancelAnimtion = true;
        isOpenAnimtion = false;
        for (int i = 0; i < mViews.size(); i++) {
            removeView(mViews.get(i));
        }
        mViews.clear();
        mXRandoms.clear();
        mYRandoms.clear();
        mYCurrentCanShoseRandoms.clear();
        mXCurrentCanShoseRandoms.clear();
        mHandler.removeCallbacksAndMessages(null);
    }

    /**
     * 设置水滴
     * @param waters
     */
    public void setWaters(final List<Water> waters) {
        if (waters == null || waters.isEmpty()) {
            return;
        }
        post(new Runnable() {
            @Override
            public void run() {
                setDatas(waters);
            }
        });
    }

    /**
     * 设置数据
     * @param waters
     */
    private void setDatas(List<Water> waters) {
        reset();
        isCancelAnimtion = false;
        setCurrentCanChoseRandoms();
        addWaterView(waters);
        setViewsSpd();
        startAnimation();
    }

    private void setCurrentCanChoseRandoms() {
        mXCurrentCanShoseRandoms.addAll(X_MAX_CHOSE_RANDOMS);
        mYCurrentCanShoseRandoms.addAll(Y_MAX_CHOSE_RANDOMS);
    }

    /**
     * 添加水滴view
     */
    private void addWaterView(List<Water> waters) {
        for (int i = 0; i < waters.size(); i++) {
            final Water water = waters.get(i);
            View view = mInflater.inflate(mChildViewRes, this, false);
            TextView tvWater = view.findViewById(R.id.tv_water);
            view.setTag(water);
            tvWater.setText(String.valueOf(water.getNumber()) + "g");
            view.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    handViewClick(view);
                }
            });
            //随机设置view动画的方向
            view.setTag(R.string.isUp, mRandom.nextBoolean());
            setChildViewLocation(view);
            mViews.add(view);
            addShowViewAnimation(view);
        }
    }

    /**
     * 添加显示动画
     * @param view
     */
    private void addShowViewAnimation(View view) {
        addView(view);
        view.setAlpha(0);
        view.setScaleX(0);
        view.setScaleY(0);
        view.animate().alpha(1).scaleX(1).scaleY(1).setDuration(ANIMATION_SHOW_VIEW_DURATION).start();
    }

    /**
     * 处理view点击
     *
     * @param view
     */
    private void handViewClick(View view) {
        //移除当前集合中的该view
        mViews.remove(view);
        Object tag = view.getTag();
        if (tag instanceof Water) {
            Water waterTag = (Water) tag;
            mTotalConsumeWater += waterTag.getNumber();
            Toast.makeText(getContext(), "当前点击的是:" + waterTag.getName() + "水滴的值是:"
                    + waterTag.getNumber() + "总的水滴数是" + mTotalConsumeWater, Toast.LENGTH_SHORT).show();
        }
        view.setTag(R.string.original_y, view.getY());
        animRemoveView(view);
    }

    /**
     * 设置view在父控件中的位置
     *
     * @param view
     */
    private void setChildViewLocation(View view) {
        view.setX((float) (maxX * getX_YRandom(mXCurrentCanShoseRandoms,mXRandoms)));
        view.setY((float) (maxY * getX_YRandom(mYCurrentCanShoseRandoms,mYRandoms)));
        view.setTag(R.string.original_y, view.getY());
    }

    /**
     * 获取x轴或是y轴上的随机值
     *
     * @return
     */
    private double getX_YRandom(List<Float> choseRandoms,List<Float> saveRandoms) {

        if (choseRandoms.size() <= 0) {
            //防止水滴别可选项的个数还要多,这里就重新对可选项赋值
            setCurrentCanChoseRandoms();
        }
        //取用一个随机数,就移除一个随机数
        float random = choseRandoms.get(mRandom.nextInt(choseRandoms.size()));
        choseRandoms.remove(random);
        saveRandoms.add(random);
        return random;
    }

    /**
     * 设置所有子view的加速度
     */
    private void setViewsSpd() {
        for (int i = 0; i < mViews.size(); i++) {
            View view = mViews.get(i);
            setSpd(view);
        }
    }

    /**
     * 设置View的spd
     *
     * @param view
     */
    private void setSpd(View view) {
        float spd = mSpds.get(mRandom.nextInt(mSpds.size()));
        view.setTag(R.string.spd, spd);
    }

    /**
     * 设置偏移
     */
    private void setOffSet() {
        for (int i = 0; i < mViews.size(); i++) {
            View view = mViews.get(i);
            float spd = (float) view.getTag(R.string.spd);
            float original = (float) view.getTag(R.string.original_y);
            float step = CHANGE_RANGE / BASE_OFFSET_MUL * spd;
            boolean isUp = (boolean) view.getTag(R.string.isUp);
            float translationY;
            if (isUp) {
                translationY = view.getY() - step;
            } else {
                translationY = view.getY() + step;
            }

            if (translationY - original > CHANGE_RANGE) {
                translationY = original + CHANGE_RANGE;
                view.setTag(R.string.isUp, true);
            } else if (translationY - original < -CHANGE_RANGE) {
                translationY = original - CHANGE_RANGE;
                setSpd(view);
                view.setTag(R.string.isUp, false);
            }
            view.setY(translationY);
        }
    }

    /**
     * 获取两个点之间的距离
     *
     * @param p1
     * @param p2
     * @return
     */
    public float getDistance(Point p1, Point p2) {
        float _x = Math.abs(p2.x - p1.x);
        float _y = Math.abs(p2.y - p1.y);
        return (float) Math.sqrt(_x * _x + _y * _y);
    }

    /**
     * 动画移除view
     * @param view
     */
    private void animRemoveView(final View view) {
        final float x = view.getX();
        final float y = view.getY();
        float space = getDistance(new Point((int) x, (int) y), mDestroyPoint);

        ValueAnimator animator = ValueAnimator.ofFloat(x, 0);
        animator.setDuration((long) (REMOVE_DELAY_MILLIS / mMaxSpace * space));
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                if (isCancelAnimtion) {
                    return;
                }
                float value = (float) valueAnimator.getAnimatedValue();
                float alpha = value / x;
                float translationY = y + (x - value) * (maxY - y) / x;
                setViewProperty(view, alpha, translationY, value);
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                removeView(view);
            }
        });
        animator.start();
    }

    /**
     * 设置view的属性
     * @param view
     * @param alpha
     * @param translationY
     * @param translationX
     */
    private void setViewProperty( View view, float alpha,float translationY,float translationX) {
        view.setTranslationY(translationY);
        view.setTranslationX(translationX);
        view.setAlpha(alpha);
        view.setScaleY(alpha);
        view.setScaleX(alpha);
    }

    /**
     * 开启水滴抖动动画
     */
    private void startAnimation() {
        if (isOpenAnimtion) {
            return;
        }

        mHandler.sendEmptyMessage(WHAT_ADD_PROGRESS);
        isOpenAnimtion = true;
    }

    /**
     * 销毁
     */
    public void onDestroy() {
        isCancelAnimtion = true;
        mHandler.removeCallbacksAndMessages(this);
    }
}