Android送花动画

Posted freeCodeSunny

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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如下:

<declare-styleable name="ScaleTextView">
    <attr name="o_color" format="reference|color"></attr>
    <attr name="i_color" format="reference|color"></attr>
    <attr name="is_border" format="boolean"></attr>
    <attr name="text_size" format="dimension"></attr>
</declare-styleable>

       上面我们采用了自定义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如下:

<declare-styleable name="FlowerGiftView">
    <attr name="left_to_right" format="boolean"></attr>
    <attr name="gift_desc" format="reference|string"></attr>
</declare-styleable>

       整个布局如下:

<?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"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content">

    <TextView
        android:id="@+id/gift_desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:drawableLeft="@drawable/rtc_icon_flower1"
        android:text="收到花"
        android:textColor="@color/white"/>

    <com.demo.opengl.widget.StrokeTextView
        android:id="@+id/gift_count"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/gift_desc"
        android:paddingLeft="6dp"
        android:paddingRight="14dp"
        android:text="x 1"
        app:draw_border="true"
        app:inner_color="@color/colorAccent"
        app:out_color="@color/yellow"/>

</RelativeLayout>

       相对布局中包含了两个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。之后我们需要设置动画。

动画效果

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

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

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

水平飞入

       首先是整个动画的飞入,我们飞入的距离为整个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:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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"
    android:background="@color/white"
    android:orientation="vertical"
    tools:context="com.demo.demo.activity.AnimActivity">

    <Button
        android:id="@+id/start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="start"/>

    <Button
        android:id="@+id/update"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="update"/>

    <com.demo.demo.widget.FlowerGiftView
        android:id="@+id/right_gift_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:layout_marginTop="100dp"
        android:background="@drawable/rtc_flowers_right"
        android:gravity="center"
        android:paddingLeft="11dp"
        android:visibility="invisible"
        app:gift_desc="送出花"></com.demo.demo.widget.FlowerGiftView>

    <com.demo.demo.widget.FlowerGiftView
        android:id="@+id/left_gift_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:background="@drawable/rtc_flowers_left"
        android:gravity="center"
        android:paddingLeft="14dp"
        android:visibility="invisible"
        app:gift_desc="收到花"
        app:left_to_right="true"></com.demo.demo.widget.FlowerGiftView>

</LinearLayout>

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

Code

       代码Git地址如下:

       Code

以上是关于Android送花动画的主要内容,如果未能解决你的问题,请参考以下文章

luogu P2073 送花

P2073 送花

Carson带你学Android:常见的动画类型你知多少?

高中化学,第四小题,有图。 T元素:最外层电子数是次外层电子数的三倍 Z元素:第三周期元素的简单离

android view动画实现从边缘滑出的效果怎么做

android view动画实现从边缘滑出的效果怎么做