Android送花动画。近期有一个需求,需要做一个送花的动画,初始点击一朵花就出现一个动画,再点一次又出现一次,就算是连击,点击多少次就出现多少次,但是感觉效果太丑,因此将连击合并在一起只更改数字。
这里先做一个demo,效果就是demo运行中截取的,大致效果如下:
多次连击只需要更新数字,不在需要重新出现动画,只有当动画执行完毕后,才出现下一次动画。
这里主要包括几个元素,一个描述文本,一个缩放的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来实现,这样有一个好处就是代码比较隔离,所有的效果都在这里处理。
从上面的动画看,我们是送出花朵,但是同时我们收到花朵动画效果也是类似的,只是靠左,向上漂移,这里我们采用自定义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就可以开始执行了。