频道栏目
首页 > 资讯 > Android > 正文

Android送花动画

17-06-19        来源:[db:作者]  
收藏   我要投稿

Android送花动画。近期有一个需求,需要做一个送花的动画,初始点击一朵花就出现一个动画,再点一次又出现一次,就算是连击,点击多少次就出现多少次,但是感觉效果太丑,因此将连击合并在一起只更改数字。

效果

这里先做一个demo,效果就是demo运行中截取的,大致效果如下:

多次连击只需要更新数字,不在需要重新出现动画,只有当动画执行完毕后,才出现下一次动画。

实现

这里主要包括几个元素,一个描述文本,一个缩放的View,再加一个背景效果。描述文本设置一个图标。缩放文本有一个文字边框效果。

缩放View

首先来实现一个缩放的View,初始首先想到的是采用自定义View,自己draw一个文本,先draw外层文本,再draw内层文本。代码如下:

package com.demo.demo.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;

import com.demo.demo.R;

public class ScaleTextView extends View {

    private Paint paint;

    private int outColor;

    private int innerColor;

    private boolean drawBorder;

    private float scale;

    private String text;

    private int dimensionPixelSize;

    public ScaleTextView(Context context) {
        super(context);
        init(context, null);
    }

    public ScaleTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);

    }

    public ScaleTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ScaleTextView);
        outColor = array.getColor(R.styleable.ScaleTextView_o_color, getResources().getColor(R.color.colorPrimary));
        innerColor = array.getColor(R.styleable.ScaleTextView_i_color, getResources().getColor(R.color.colorAccent));
        drawBorder = array.getBoolean(R.styleable.ScaleTextView_is_border, false);
        dimensionPixelSize = array.getDimensionPixelSize(R.styleable.ScaleTextView_text_size, getResources()
                .getDimensionPixelSize(R.dimen.inSize));
        array.recycle();
        outPaint();
    }

    private void outPaint() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setTextSize(dimensionPixelSize);
    }

    public float getScale() {
        return scale;
    }

    public void setScale(float scale) {
        this.scale = scale;
        setScaleX(scale);
        setScaleY(scale);
    }

    public void setText(String text) {
        this.text = text;
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(dimensionPixelSize * 4, dimensionPixelSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (TextUtils.isEmpty(text)) {
            return;
        }
        if (drawBorder) {
            paint.setColor(outColor);
            paint.setStrokeWidth(5);
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
            canvas.drawText(text, 0, dimensionPixelSize, paint);
        }
        paint.setColor(innerColor);
        paint.setStrokeWidth(0);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        canvas.drawText(text, 0, dimensionPixelSize, paint);
    }
}

这里先不用管scale参数,这是为后面的属性动画设置的属性进行缩放动画。我们在onMeasure强制设置宽高,高就是文本的大小,宽就是最大文本的大小。
当有文本设置时,调用invalidate进行重绘,在onDraw中绘制文本,先绘制外层,再绘制内存,分别用不同的颜色。在构造的时候解析attr,attr如下:


    
    
    
    

上面我们采用了自定义View来展示,需要我们传入文本大小,文本颜色,我在网上看到有人用TextView的方式实现了同样的效果,这里采用自定义TextView来实现同样的效果,代码如下:

package com.demo.demo.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.widget.TextView;

import com.demo.demo.R;

import java.lang.reflect.Field;

public class StrokeTextView extends android.support.v7.widget.AppCompatTextView {

    private TextPaint paint;

    private int outColor;

    private int innerColor;

    private boolean drawBorder;

    private float scale;

    public StrokeTextView(Context context) {
        super(context);
        init(context, null);
    }

    public StrokeTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);

    }

    public StrokeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        paint = this.getPaint();
        paint.setAntiAlias(true);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StrokeTextView);
        outColor = array.getColor(R.styleable.StrokeTextView_out_color, getResources().getColor(R.color.yellow));
        innerColor = array.getColor(R.styleable.StrokeTextView_inner_color, getResources().getColor(R.color
                .colorAccent));
        drawBorder = array.getBoolean(R.styleable.StrokeTextView_draw_border, false);
        array.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (drawBorder) {
            // 描外层
            setTextColorUseReflection(outColor);
            paint.setStrokeWidth(5);
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
            super.onDraw(canvas);

            // 描内层,恢复原先的画笔
            setTextColorUseReflection(innerColor);
            paint.setStrokeWidth(0);
            paint.setStyle(Paint.Style.FILL_AND_STROKE);

        }
        super.onDraw(canvas);
    }

    /**
     * 使用反射的方法进行字体颜色的设置
     *
     * @param color
     */
    private void setTextColorUseReflection(int color) {
        Field textColorField;
        try {
            textColorField = TextView.class.getDeclaredField("mCurTextColor");
            textColorField.setAccessible(true);
            textColorField.set(this, color);
            textColorField.setAccessible(false);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        paint.setColor(color);
    }

    public float getScale() {
        return scale;
    }

    public void setScale(float scale) {
        this.scale = scale;
        setScaleX(scale);
        setScaleY(scale);
    }
}

attr定义方式与上面的类似,采用反射获取当前TextView的颜色,之后更改颜色,先绘制外层,再恢复颜色只,调用系统绘制方式。

组合View

这里整个组合效果我们也采用自定义View来实现,这样有一个好处就是代码比较隔离,所有的效果都在这里处理。

从上面的动画看,我们是送出花朵,但是同时我们收到花朵动画效果也是类似的,只是靠左,向上漂移,这里我们采用自定义attr来控制方向与文本描述符,attr如下:


    
    

整个布局如下:




    

    

相对布局中包含了两个view,一个作为描述View,一个作为缩放View,如果还有定制的效果。可以继续设置View,整个组合View的代码如下:

package com.demo.demo.widget;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.demo.demo.R;

public class FlowerGiftView extends RelativeLayout {

    /**
     * 是否从左到右
     */
    private boolean left2Right;

    /**
     * gift desc
     */
    private String desc;

    /**
     * 设置文案描述
     */
    private TextView giftDesc;

    /**
     * 缩放动画
     */
    private StrokeTextView giftCount;


    public FlowerGiftView(Context context) {
        super(context);
        init(context, null);
    }

    public FlowerGiftView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public FlowerGiftView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FlowerGiftView);
        left2Right = array.getBoolean(R.styleable.FlowerGiftView_left_to_right, false);
        desc = array.getString(R.styleable.FlowerGiftView_gift_desc);
        array.recycle();
        View inflate = inflate(getContext(), R.layout.gift_view_layout, this);
        findViews(inflate);
    }

    private void findViews(View view) {
        giftDesc = (TextView) view.findViewById(R.id.gift_desc);
        giftDesc.setText(desc);
        giftCount = (StrokeTextView) view.findViewById(R.id.gift_count);
    }

}

这里我们就完成了组合View。之后我们需要设置动画。

动画效果

这里我们采用属性动画来实现,首先我们分析一下整个动画:

整个View从屏幕外水平移入 如果是连击则文本多次更改并缩放 最后动画向下或者向上飞出

这里我们先拆解了动画的几个步骤,我们分别来实现各个动画:

水平飞入

首先是整个动画的飞入,我们飞入的距离为整个View的宽度,因此我们需要获取View的宽高。因此我们首先获取View的宽高,我们可以在onSizeChanged的会调用中获取:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    transX = this.getMeasuredWidth();
    transY = this.getMeasuredHeight();
}

这里获取了View的宽高,高是动画飞出时需要用到的参数。接着我们来实现飞入的动画:

@NonNull
private ObjectAnimator getTrans() {
    ObjectAnimator trans = ObjectAnimator.ofFloat(this, "translationX", left2Right ? -transX : transX, 0);
    trans.setDuration(300);
    return trans;
}

这里判断view放置的位置,如果再右边,需要从右向左飞入,如果再在左边,需要从左向右飞入,

缩放动画

文本缩放需要显示多次,因此我们需要一个重复的缩放动画,重复次数需要由外部传入:

private ObjectAnimator getScale(int count) {
    scale = ObjectAnimator.ofFloat(giftCount, "scale", 1.0f, 2.0f, 1.0f);
    scale.setDuration(500);
    scale.setRepeatCount(count - 1);
    scale.setRepeatMode(ObjectAnimator.RESTART);
    scale.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationRepeat(Animator animation) {
            super.onAnimationRepeat(animation);
            giftCount.setText("x " + (++incTimes));
        }
    });
    return scale;
}

这里首先设置了一个先放大再缩小的动画,之后设置重复次数,在回调中更改显示的文本。这里我们将scale作为一个全局变量,主要是因为需要更改重复次数。
这里需要注意的是,这里使用的属性是scale,这就是我们前面设置的属性。在scale里面我们调用了setScaleX与setScaleY,不然我们就需要实现两个动画才能达到缩放的效果。

飞出动画

最后一个是一个向上飞出的动画,移动的距离就是整个View的高度:

@NonNull
private ObjectAnimator getFlyFade() {
    PropertyValuesHolder flyUp = PropertyValuesHolder.ofFloat("translationY", 0, left2Right ? -transY : transY);
    PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1, 0);
    return ObjectAnimator.ofPropertyValuesHolder(this, flyUp, alpha).setDuration(500);
}

动画在飞出的过程中逐渐消失,主要采用更改透明度来实现。

组合

上面的动画我们需要序列执行,先执行飞入的动画,在执行缩放的动画,在执行飞出的动画,我们可以采用对每一动画加入监听,前一个执行完毕后执行下一个,也可以采用AnimatorSet,指定序列执行:

set.setInterpolator(new AccelerateDecelerateInterpolator());
set.playSequentially(getTrans(), getScale(count), getFlyFade());
set.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        isAnimation = false;
        FlowerGiftView.this.setVisibility(INVISIBLE);
        animate().alpha(1).translationY(0).start();
    }

    @Override
    public void onAnimationStart(Animator animation) {
        super.onAnimationStart(animation);
        isAnimation = true;
        FlowerGiftView.this.setVisibility(VISIBLE);
        giftCount.setText("x 1");
        incTimes = 1;
    }
});
set.start();

这里主要在动画开始时候显示View,同时增长变量从1开始,在结束动画时候,重置动画,为下一次做好准备。

重复动画

上面的动画只是执行一次就结束,如果再执行过程中又有点击事件发生,我们怎么才能在动画上自动更改?这里主要是更改scale的缩放次数,代码如下:

/**
 * 如果动画不存在,先创建再执行
 * 如果正在运行,则直接更新执行次数
 * 如果没执行, 则启动执行
 * 当动画执行完成后,判断RepeatCount==incTimes,相等表示执行完成了。否则执行剩下的值
 *
 * @param count
 */
public void startAnim(int count) {
    if (set == null) {
        set = new AnimatorSet();
        set.setInterpolator(new AccelerateDecelerateInterpolator());
        set.playSequentially(getTrans(), getScale(count), getFlyFade());
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                isAnimation = false;
                FlowerGiftView.this.setVisibility(INVISIBLE);
                animate().alpha(1).translationY(0).start();
                int remainTimes = scale.getRepeatCount() - incTimes;
                if (remainTimes > 0) {
                    startAnim(remainTimes);
                }
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                isAnimation = true;
                FlowerGiftView.this.setVisibility(VISIBLE);
                giftCount.setText("x 1");
                incTimes = 1;
            }
        });
        set.start();
    } else {
        if (isAnimation) {
            scale.setRepeatCount(scale.getRepeatCount() + count);
        } else {
            scale.setRepeatCount(count - 1);
            set.start();
        }
    }
}

如果动画不存在,表示还没有执行过先初始化,在执行,如果动画在执行过程中,则更改缩放次数,在整个动画的结束判断自增长与总数是否相同,如果不相同说明设置的时候,执行实际已过,需要执行剩下的次数,否则就重新设置缩放次数进行执行。

上面我们分步骤讲述了代码,下面是整个代码:

package com.demo.demo.widget;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.demo.demo.R;


public class FlowerGiftView extends RelativeLayout {

    /**
     * 动画执行状态
     */
    private boolean isAnimation;

    /**
     * 是否从左到右
     */
    private boolean left2Right;

    /**
     * gift desc
     */
    private String desc;

    /**
     * 设置文案描述
     */
    private TextView giftDesc;

    /**
     * 缩放动画
     */
    private StrokeTextView giftCount;

    /**
     * 显示次数变动
     */
    private int incTimes = 0;

    /**
     * x方向移动位置
     */
    private int transX;

    /**
     * y方向移动位置
     */
    private int transY;

    private ObjectAnimator scale;

    private AnimatorSet set;

    public FlowerGiftView(Context context) {
        super(context);
        init(context, null);
    }

    public FlowerGiftView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public FlowerGiftView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FlowerGiftView);
        left2Right = array.getBoolean(R.styleable.FlowerGiftView_left_to_right, false);
        desc = array.getString(R.styleable.FlowerGiftView_gift_desc);
        array.recycle();
        View inflate = inflate(getContext(), R.layout.gift_view_layout, this);
        findViews(inflate);
    }

    private void findViews(View view) {
        giftDesc = (TextView) view.findViewById(R.id.gift_desc);
        giftDesc.setText(desc);
        giftCount = (StrokeTextView) view.findViewById(R.id.gift_count);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        transX = this.getMeasuredWidth();
        transY = this.getMeasuredHeight();
    }

    /**
     * 如果动画不存在,先创建再执行
     * 如果正在运行,则直接更新执行次数
     * 如果没执行, 则启动执行
     * 当动画执行完成后,判断RepeatCount==incTimes,相等表示执行完成了。否则执行剩下的值
     *
     * @param count
     */
    public void startAnim(int count) {
        if (set == null) {
            set = new AnimatorSet();
            set.setInterpolator(new AccelerateDecelerateInterpolator());
            set.playSequentially(getTrans(), getScale(count), getFlyFade());
            set.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    isAnimation = false;
                    FlowerGiftView.this.setVisibility(INVISIBLE);
                    animate().alpha(1).translationY(0).start();
                    int remainTimes = scale.getRepeatCount() - incTimes;
                    if (remainTimes > 0) {
                        startAnim(remainTimes);
                    }
                }

                @Override
                public void onAnimationStart(Animator animation) {
                    super.onAnimationStart(animation);
                    isAnimation = true;
                    FlowerGiftView.this.setVisibility(VISIBLE);
                    giftCount.setText("x 1");
                    incTimes = 1;
                }
            });
            set.start();
        } else {
            if (isAnimation) {
                scale.setRepeatCount(scale.getRepeatCount() + count);
            } else {
                scale.setRepeatCount(count - 1);
                set.start();
            }
        }
    }

    @NonNull
    private ObjectAnimator getFlyFade() {
        PropertyValuesHolder flyUp = PropertyValuesHolder.ofFloat("translationY", 0, left2Right ? -transY : transY);
        PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1, 0);
        return ObjectAnimator.ofPropertyValuesHolder(this, flyUp, alpha).setDuration(500);
    }

    private ObjectAnimator getScale(int count) {
        scale = ObjectAnimator.ofFloat(giftCount, "scale", 1.0f, 2.0f, 1.0f);
        scale.setDuration(500);
        scale.setRepeatCount(count - 1);
        scale.setRepeatMode(ObjectAnimator.RESTART);
        scale.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
                giftCount.setText("x " + (++incTimes));
            }
        });
        return scale;
    }

    @NonNull
    private ObjectAnimator getTrans() {
        ObjectAnimator trans = ObjectAnimator.ofFloat(this, "translationX", left2Right ? -transX : transX, 0);
        trans.setDuration(300);
        return trans;
    }
}

使用

上面讲述了整个View的构建,但是还没有使用过,界面中怎么使用View:


这里只是需要注意的是,初始我们设置的是invisible,因为我们需要获取到整个View的宽高,如果设置gone则获取不到宽高。
最终在界面中只需要调用startAnim就可以开始执行了。

相关TAG标签
上一篇:Fragment的基本使用
下一篇:我“勾搭”了20位产品总监,他们说做到这3点,就是一款「好产品」
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站