关于Android项目中的Toast那些动画实现方式

最近产品给了一个竞品App的Toast动画,希望开发可以去实现它,经过一段时间的深(不)思(停)熟(百)虑(度)之后,发现事情其实并不简单,所以这里记录一下关于Android~Toast动画实现的相关问题。

首先产品动画大概长这样:

https://live.csdn.net/v/172131

动画非常简单,大概可以分解为:

  • 弹出:位置平移和透明度增加;

  • 回弹:位置回弹和透明度减少;

其实在我们实际项目中,我们肯定希望这个Toast可以动态配置,弹出的位置,宽高以及弹出的动画等等,基于这些网络上一些开源的Toast框架也不少,大部分都可以满足,重复的*咱也不必重复造,这篇文章的目的主要是对Toast动画实现的核心进行讨论,各有长短,对于Android的各个版本的适配情况。

目前实现Toast动画主流实现大概有三种方式:WindowManager,反射获取TN对象以及LayoutTransition。

一、WindowManger

其实Toast的底层也是通过WindowManger来实现的,并且设置WindowManager的type为TYPE_TOAST,咱要是自己设置Toast动画,必定要自己实现WindowManger,所以核心代码为:

...
//首先获取WindowManger对象
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
...
mToast = new Toast(getContext());
mToast.setView(layout);
mParams = new WindowManager.LayoutParams();
mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.format = PixelFormat.TRANSLUCENT;
mParams.windowAnimations = R.style.AgreeToastStyle;//设置进入退出动画效果
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
    mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    mParams.type = WindowManager.LayoutParams.TYPE_TOAST;
}
mParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
mParams.y = mContext.getResources().getDimensionPixelOffset(R.dimen.dp_92);
​
public synchronized void show(@Nullable String msg) {
  if (!isShow && !TextUtils.isEmpty(msg)) {
      isShow = true;
      mBinding.tvTitle.setText(msg);
      mWindowManager.addView(mToast.getView(), mParams);
      mTimer = new Timer();
      mTimer.schedule(new TimerTask() {
              @Override
              public void run() {
                isShow = false;
                mWindowManager.removeView(mToast.getView());
        }
     }, mDuration);
  }
}

嗯嗯嗯,写好了,快乐了哦,下班。。。

Boom~

android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@e44fd78 -- permission denied for window type 2038
        at android.view.ViewRootImpl.setView(ViewRootImpl.java:1024)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:428)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:118)
        at com.wei.campus_today.ui.widget.AgreeToast.show(AgreeToast.java:88)
        at com.wei.campus_today.ui.widget.AgreeToast.show(AgreeToast.java:101)
        at com.wei.campus_today.ui.activity.LoginActivity.checkoutAgreeSelected(LoginActivity.java:125)
        at com.wei.campus_today.ui.activity.LoginActivity.onClick(LoginActivity.java:161)
        at android.view.View.performClick(View.java:7192)
        at android.view.View.performClickInternal(View.java:7166)
        at android.view.View.access$3500(View.java:824)
        at android.view.View$PerformClick.run(View.java:27592)
        at android.os.Handler.handleCallback(Handler.java:888)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:213)
        at android.app.ActivityThread.main(ActivityThread.java:8178)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)

首先在Android8.0以上,WindowManger的Type必须设置TYPE_APPLICATION_OVERLAY,再者还得动态获取权限:android.permission.SYSTEM_ALERT_WINDOW,但是在竞品App中,弹出这个Toast的时候,并没有要求获取Window权限啊~

二、反射获取TN对象

如果咱这不能自定义Window Manger来实现动画,那么咱可不可以获取Toast依赖的WindowManger,直接设置动画呢?那么这样我们不必执行Toast的时候,需要获取Window权限。

说干就干,干完早点干饭~

打开Toast源码,发现其中有一个TN对象,其中持有WindowManager的对象,那么咱可以使用反射,设置TN中WindowManger的windowAnimations为我们自定义的动画ID。

    public synchronized void show(@Nullable String msg) {
        if (!isShow && !TextUtils.isEmpty(msg)) {
            isShow = true;
            try {
                Object mTN;
                Field field = mToast.getClass().getDeclaredField("mTN");
                field.setAccessible(true);
                mTN = field.get(mToast);
                if (mTN != null) {
                    Field field1 = mTN.getClass().getField("mParams");
                    field1.setAccessible(true);
                    Object mParams = field1.get(mTN);
                    if (mParams != null
                            && mParams instanceof WindowManager.LayoutParams) {
                        WindowManager.LayoutParams params = (WindowManager.LayoutParams) mParams;
                        params.windowAnimations = R.style.AgreeToastStyle;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            mToast.show();
        }
    }

嗯嗯,运行好了,没问题,下班~

但是Android10.0上运行,效果还是没了,还是基础效果,打开面板一看报错了:

java.lang.NoSuchFieldException: No field mTN in class Landroid/widget/Toast; (declaration of 'android.widget.Toast' appears in /system/framework/framework.jar!classes3.dex) at java.lang.Class.getDeclaredField(Native Method)

看来今儿是没办法按时下班了,默默的打开了美团~

再次打开Toast源码,仔细的开始研究...

 private static class TN extends ITransientNotification.Stub {
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
        private final WindowManager.LayoutParams mParams;
 }

其实这个也是网上说的Android系统的灰色权限,高于28的版本没办法通过反射拿到这个对象,那么现在只剩下唯一的一条路了,通过自定义View实现LayoutTransition

三、LayoutTransition

咱可以完全的抛弃掉Toast,通过自定义View实现一个基础的TextView,在show的时候通过ViewGroup.addView将基础的TextView加入到容器中,这时候可以设置ViewGroup的LayoutTransition实现动画。但是这样的逻辑会有两个问题:

  • 过度依赖ViewGroup,若不是在show的时候,需要传入Activity/Fragment,然后通过findViewById去获取根布局,然后添加自定义View?

  • 如果依赖的Activity/Fragment没有设置setContentView,那么如何通过通过findViewById去获取ViewGroup呢?

1.解决过度依赖Activity/Fragment问题:

既然选择了这个方案,那么在展示自定义View的时候必定需要ViewGroup,为了避免耦合,那么咱可以集成Application.ActivityLifecycleCallbacks,实现Activity栈,在Application中注册,即可获取栈顶的Activity来展示这个View~

2.解决依赖的Activity/Fragment没有设置setContentView,如何获取ViewGroup?

回答这个问题的时候,我们必须知道activity的窗口层级

关于Android项目中的Toast那些动画实现方式

我们可以通过android.R.id.content来获取Activity的根布局的FrameLayout,无论你设不设置SetContentView都可以拿到ViewGroup

关于LayoutTransition一些介绍,在ViewGroup.addView/removeView的时候,可以将动画带给需要的View。

相关资料

上一篇:Android面试官:Window连环十二问你顶得住吗?,flutter面试题


下一篇:Android 悬浮窗口