动画还可以这么玩?使用 Toast 显示 or Dialog 显示

本篇不是讲解动画的设计,而是分析动画在使用过程中,如何合理显示遇到的一些坑,主要是由于特定场景引起的。

问题

相信大家都见过这样的点赞动画,点赞之后图片能够飘一会。

思路:动画其实并不难,通过一个自定义 View,大小为显示动画的范围,通过一个 ImageView显示图片,然后通过动画根据设计的路径改变位置,透明度和大小,显示特定的时长。

思路有了,然后就是实现,实现完成之后就出现了坑,坑不在这个动画上,而是使用的场景。看一下这个图

Item

典型的 RecyclerView 的多布局,这样的布局如果设计:


思路一在每个 Item 如果设计成一个布局这个布局会很大也就是 1 234 这几部分在一个子布局中那么这个动画没有问题点击按钮后正常显示自定义 View 可以放在子布局中的最上层但存在一个问题当需要更新每个 Item 难道要刷新整个 Item举个例子用户在看视频点赞之后因为需要刷新头像列表所以整个 Item 刷新了一下视频也重新加载屏幕会闪一下体验非常不好所以这种思路不能接受

思路二 Item 拆分的更细1234 每个部分各自为一个小模块刷新时只刷新单独一个模块这样不会出现屏幕闪一下的问题闪一下主要是由于视频或者大图部分刷新导致的),这样做的好处还有使得布局更加清晰这部分的实现不具体展开了采用的 BRVAH 封装的 RecyclerView但是存在一个问题动画属于第 3 部分而第 3 部分的高度不足以装下动画布局的大小我试过之后动画仅仅展示一小部分因为头像这部分的高度不够这就是问题的关键这个问题怎么解决将动画部分放在第 2 部分但是不同布局模块间的通讯怎么实现又是问题

所有也就引出了本文的标题,在布局最上层显示动画,可以采用 Dialog 或者 Toast。开始想到的是 Dialog,通过自定义 Dialog 来显示动画

自定义 View

采用 Dialog 或者 Toast显示动画都需要有一个自定义 View,这个 View 才是显示动画实际载体,然后将这个 View 放到 Dialog 或者 Toast 上。自定义 View 这部分就不细讲了,大体思路就是通过继承 RelativeLayout,然后添加一个 ImageView,用来显示图片,并通过设置动画,让图片动起来,并自己设置估值器,设置图片飘动的路径,动画由移动,缩放,透明度 3˙种动画组合的,下面给出代码。


public class PraiseAnimView extends RelativeLayout {

private static final int TIME_ANIMATION = 4000;
protected PointF pointFStart, pointFEnd, pointFFirst, pointFSecond;
private Bitmap mBitmap;
private AnimatorSet mAnimatorSet;
private ImageView mImageView;

public PraiseAnimView(Context context) {
super(context);
init();
}

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

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

private void init() {
setBackground(getResources().getDrawable(R.mipmap.bg_praise_anim, null));
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.praise_anim_pic);
int height = Double.valueOf(bitmap.getHeight() * 1.5).intValue();
int width = Double.valueOf(bitmap.getWidth() * 1.5).intValue();
mBitmap = BitmapUtil.zoomImg(bitmap, width, height);
pointFStart = new PointF();
pointFFirst = new PointF();
pointFSecond = new PointF();
pointFEnd = new PointF();
initAnim();
}

private void initView() {
mImageView = new ImageView(getContext());
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.addRule(CENTER_HORIZONTAL);
params.addRule(ALIGN_PARENT_BOTTOM);
mImageView.setImageBitmap(mBitmap);
addView(mImageView, params);
}

public void startAnim() {
removeAllViews();
initView();
mAnimatorSet.setStartDelay(0);
mAnimatorSet.start();
}

private void initAnim() {
mAnimatorSet = new AnimatorSet();
// 位置动画
ValueAnimator beiAnim = ValueAnimator.ofObject(new MyTypeEvaluator(pointFFirst, pointFSecond),
pointFStart, pointFEnd);
beiAnim.addUpdateListener(animation -> {
PointF value = (PointF) animation.getAnimatedValue();
mImageView.setX(value.x - mImageView.getWidth() / 2);
mImageView.setY(value.y + mImageView.getHeight() / 2);
});
beiAnim.setInterpolator(new AccelerateDecelerateInterpolator());
// 缩放动画
PropertyValuesHolder pl = PropertyValuesHolder.ofFloat("scaleY", 1f, 1.2f, 1f);
PropertyValuesHolder p2 = PropertyValuesHolder.ofFloat("scaleX", 1f, 1.2f, 1f);
ObjectAnimator scaleAnim = ObjectAnimator.ofPropertyValuesHolder(mImageView, pl, p2).setDuration(500);
// 透明度动画
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mImageView, "alpha", 0.8f, 0).setDuration(500);
mAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}

@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
PraiseAnimView.this.removeView(mImageView);
}
});
mAnimatorSet.setDuration(TIME_ANIMATION).play(beiAnim).with(alphaAnim).with(scaleAnim);
}

@Override
public void draw(Canvas canvas) {
super.draw(canvas);

pointFStart.x = mBitmap.getWidth() / 2;
pointFStart.y = getMeasuredHeight() - mBitmap.getHeight() * 3 / 2;
pointFEnd.y = 10;
pointFEnd.x = getMeasuredWidth() - mBitmap.getWidth() / 2;

pointFFirst.x = 10;
pointFFirst.y = getMeasuredHeight() / 2;
pointFSecond.x = getMeasuredWidth();
pointFSecond.y = getMeasuredHeight() / 2;
}

/**
* 估值器
*/
static class MyTypeEvaluator implements TypeEvaluator<PointF> {

private PointF pointFFirst, pointFSecond;

public MyTypeEvaluator(PointF start, PointF end) {
this.pointFFirst = start;
this.pointFSecond = end;
}

@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
PointF result = new PointF();
float left = 1 - fraction;
result.x = (float) (startValue.x * Math.pow(left, 3) + 3 * pointFFirst.x * Math.pow(left, 2)
* fraction + 3 * pointFSecond.x * Math.pow(fraction, 2) * left + endValue.x * Math.pow(fraction, 3));
result.y = (float) (startValue.y * Math.pow(left, 3) + 3 * pointFFirst.y * Math.pow(left, 2)
* fraction + 3 * pointFSecond.y * Math.pow(fraction, 2) * left + endValue.y * Math.pow(fraction, 3));
return result;
}
}

}

使用 Dialog 显示动画

有了自定义的 View,那么接下来就简单了,将这个 view 放到自定义 Dialog 的布局中,然后自定义 Dialog

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">

<wang.ralf.showanimation.PraiseAnimView
android:id="@+id/praise_anim_view"
android:layout_width="100dp"
android:layout_height="200dp" />

</LinearLayout>

Dialog 的代码也直接给出

public class PraiseDialog extends Dialog {

private Context mContext;
private PraiseAnimView mAnimView;
private int mWidth;
private int mHeight;
private Handler mHandler;

public PraiseDialog(Builder builder) {
super(builder.context);
mContext = builder.context;
mWidth = builder.width;
mHeight = builder.height;
mHandler = new Handler();
}

public PraiseDialog(Builder builder, int themeResId) {
super(builder.context, themeResId);
mContext = builder.context;
mWidth = builder.width;
mHeight = builder.height;
mHandler = new Handler();
}

protected PraiseDialog(Builder builder, boolean cancelable, @Nullable OnCancelListener cancelListener) {
super(builder.context, builder.cancelTouchOut, builder.cancelListener);
mContext = builder.context;
mWidth = builder.width;
mHeight = builder.height;
mHandler = new Handler();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = View.inflate(mContext, R.layout.layout_dialog_praise_animation, null);
setContentView(view);

Window win = getWindow();
WindowManager.LayoutParams lp = win.getAttributes();
lp.height = mHeight;
lp.width = mWidth;
win.setAttributes(lp);
mAnimView = view.findViewById(R.id.praise_anim_view);
}

@Override
public void show() {
super.show();
mAnimView.startAnim();
// 隐藏
mHandler.postDelayed(this::dismiss, 3500);
}

/**
* 设置对话框的位置,在屏幕上的位置
*
* @param x 横坐标
* @param y 纵坐标
*/
public void setPosition(int x, int y) {
Window win = getWindow();
WindowManager.LayoutParams lp = win.getAttributes();
win.setGravity(Gravity.TOP | Gravity.START);
lp.x = x;
lp.y = y;
win.setAttributes(lp);
}

public static final class Builder {

private Context context;
private int height, width;
private boolean cancelTouchOut;
private View view;
private int resStyle = -1;
private OnCancelListener cancelListener;


public Builder(Context context) {
this.context = context;
}

public Builder view(int resView) {
view = LayoutInflater.from(context).inflate(resView, null);
return this;
}

public Builder heightdp(int val) {
height = SizeUtils.dip2px(context, val);
return this;
}

public Builder widthdp(int val) {
width = SizeUtils.dip2px(context, val);
return this;
}

public Builder heightDimenRes(int dimenRes) {
height = context.getResources().getDimensionPixelOffset(dimenRes);
return this;
}

public Builder widthDimenRes(int dimenRes) {
width = context.getResources().getDimensionPixelOffset(dimenRes);
return this;
}

public Builder style(int resStyle) {
this.resStyle = resStyle;
return this;
}

public Builder cancelTouchout(boolean val) {
cancelTouchOut = val;
return this;
}

public Builder cancelListener(OnCancelListener listener) {
cancelListener = listener;
return this;
}

public Builder addViewOnclick(int viewRes, View.OnClickListener listener) {
view.findViewById(viewRes).setOnClickListener(listener);
return this;
}


public PraiseDialog build() {
if (resStyle != -1) {
return new PraiseDialog(this, resStyle);
} else {
return new PraiseDialog(this);
}
}
}
}

通过 Builder 模式设置 Dialog 的参数,其中有几点需要注意的:

  • 1、Dialog 的宽高设置

  • 2、动画开启以及显示时长控制

  • 3、Dialog 背景透明设置

  • 4、Dialog 显示位置控制

第 1 点:代码中已经给出

Window win = getWindow();
WindowManager.LayoutParams lp = win.getAttributes();
lp.height = mHeight;
lp.width = mWidth;
win.setAttributes(lp);

通过 Window 来设置宽高,也要注意 dp 和 pixel 的转换关系

第 2 点:动画开启比较简单,重写 show 方法,获取自定义 view,即可开启动画。显示时长这里采用的是使用 Handler 发送一个延时消息来是 Dialog 销毁掉。

第 3 点:设置 Dialog 为透明,可以通过设置 Dialog 的主题即可。

<style name="TransparentDlgTheme" parent="@android:style/Theme.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">false</item>
<item name="android:backgroundDimEnabled">false</item>
</style>

第 4 点:自定义 Dialog 中已经给出接口来设置位置

/**
* 设置对话框的位置,在屏幕上的位置
*
* @param x 横坐标
* @param y 纵坐标
*/
public void setPosition(int x, int y) {
Window win = getWindow();
WindowManager.LayoutParams lp = win.getAttributes();
win.setGravity(Gravity.TOP | Gravity.START);
lp.x = x;
lp.y = y;
win.setAttributes(lp);
}

假设需求是要显示在点击按钮的右上角位置,我们需要获取窗口或者屏幕的绝对坐标,然后将坐标的横坐标向右移动 控件的宽度,纵坐标向上 Dialog 的高度 + 按钮的高度。但是有一点问题,高度上和宽度上有一些偏差,具体问题原因还没找出来?知道的大佬可以在评论区指正。

int dialogBtnWidth = mDialogBtn.getWidth();
int height = mDialogBtn.getHeight();
//获取在整个窗口内的绝对坐标
int[] location = new int[2];
mDialogBtn.getLocationInWindow(location);
PraiseDialog dialog = new PraiseDialog.Builder(this)
.heightdp(200)
.widthdp(160)
.style(R.style.TransparentDlgTheme)
.cancelTouchout(true)
.build();
dialog.setPosition(location[0] + dialogBtnWidth,
location[1] - SizeUtils.dip2px(this, 200) - height / 2);
dialog.show();

这个显示在本例中没问题,但是在开篇的布局中,仍旧会有问题,这个坐标在 BRVAH 的 convert 方法中是获取不到的,因为布局还没有绘制完,只有在点击按钮时将坐标传过去,这里有一个小窍门,可以通过 Entity 中增加坐标字段,通过这两个字段,在刷新 item 时传过去。

本例子中使用 Dialog 显示的效果如下:

Dialog_Anim

那么使用 Dialog 的问题在哪里,为什么还需要使用 Toast 么?仅仅是为了增加一种思路么?是因为它和 Toast 是有区别的。区别在于 Dialog 会获取焦点,当显示动画的3秒钟之间:

(1)如果设置触摸 Dialog 外部 Dialog消失,这时候点击点赞按钮就被拦截掉了,需要 Dialog 消失后再点击才会起作用。这里我没有继续往下探索,有兴趣的童鞋可以继续研究下。比如,将背景设置透明的, Dialog 显示时,点击点赞按钮,仍能够做出取消点赞的响应。

(2)如果 Dialog 外部不能点击,那只有动画展示完毕,Dialog 消失,用户才能够点击,也不够友好。

左右尝试到这里,我就使用 Toast 了。

使用 Toast 显示动画

自定义 Toast 比较简单,只需通过 LayoutInflater 加载布局,然后调用 Toast 的 setView 方法即可。这里引入了 Handler 和 弱引用,保证每次只显示一个 Toast,并且不会造成内存泄露等问题。

public class AnimToast {

private static WeakReference<Toast> sWeakToast;
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
private static final int COLOR_DEFAULT = 0xFEFFFFFF;
private static int sGravity = -1;
private static int sXOffset = -1;
private static int sYOffset = -1;

private AnimToast() {
throw new UnsupportedOperationException("u can't instantiate me...");
}


/**
* Cancel the toast.
*/
public static void cancel() {
final Toast toast;
if (sWeakToast != null && (toast = sWeakToast.get()) != null) {
toast.cancel();
sWeakToast = null;
}
}

/**
* reset default position
*/
public static void resetDefaultPosition(Context context) {
setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
0, SizeUtils.dip2px(context, 24));
}

/**
* Show custom toast for a short period of time.
*
* @param layoutId ID for an XML layout resource to load.
*/
public static View showCustomShort(Context context, @LayoutRes final int layoutId) {
final View view = getView(context, layoutId);
show(context, view, Toast.LENGTH_SHORT);
return view;
}

/**
* Show custom toast for a long period of time.
*
* @param layoutId ID for an XML layout resource to load.
*/
public static View showCustomLong(Context context, @LayoutRes final int layoutId) {
final View view = getView(context, layoutId);
show(context, view, Toast.LENGTH_LONG);
return view;
}

public static void show(Context context, final View view, final int duration) {
HANDLER.post(() -> {
cancel();
final Toast toast = new Toast(context);
sWeakToast = new WeakReference<>(toast);

toast.setView(view);
toast.setDuration(duration);
if (sGravity != -1 || sXOffset != -1 || sYOffset != -1) {
toast.setGravity(sGravity, sXOffset, sYOffset);
}
toast.show();
});
}

/**
* Set the gravity.
*
* @param gravity The gravity.
* @param xOffset X-axis offset, in pixel.
* @param yOffset Y-axis offset, in pixel.
*/
public static void setGravity(final int gravity, final int xOffset, final int yOffset) {
sGravity = gravity;
sXOffset = xOffset;
sYOffset = yOffset;
}

private static View getView(Context context, @LayoutRes final int layoutId) {
LayoutInflater inflate =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return inflate != null ? inflate.inflate(layoutId, null) : null;
}
}

如果自己使用的是全局的 Toast 工具类,那么调整位置后,需要重置 Toast 的位置,

/**
* reset default position
*/
public static void resetDefaultPosition(Context context) {
setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
0, SizeUtils.dip2px(context, 24));
}

/**
* Set the gravity.
*
* @param gravity The gravity.
* @param xOffset X-axis offset, in pixel.
* @param yOffset Y-axis offset, in pixel.
*/
public static void setGravity(final int gravity, final int xOffset, final int yOffset) {
sGravity = gravity;
sXOffset = xOffset;
sYOffset = yOffset;
}

为什么是 24 dp,这个可以从 Toast 的源码中可以得知,这个自己查看一下吧。

int toastBtnWidth = mToastBtn.getWidth();
int height = mToastBtn.getHeight();
//获取在整个窗口内的绝对坐标
int[] location = new int[2];
mToastBtn.getLocationInWindow(location);
int x = location[0] + toastBtnWidth;
int y = location[1] - SizeUtils.dip2px(this, 200) - height / 2;
AnimToast.setGravity(Gravity.START | Gravity.TOP, x, y);
View view = AnimToast.showCustomLong(this, R.layout.layout_toast_praise_animation);
PraiseAnimView praiseAnimView = view.findViewById(R.id.praise_anim_view);
praiseAnimView.startAnim();

这里代码应该能够看懂,和上面的 Dialog 位置的计算一样。下面来看一下效果:

Toast_Anim

这回可以随便点击点赞和取消点赞,再也不用担心了,而且相比之下,使用 Toast 更加优美,哈哈!

Demo 代码地址

练习代码

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦