关于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的窗口层级

img

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

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

相关资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,我可以为您提供一些Android Java Toast自定义动画的代码,以下是示例代码: 1. 在res目录下新建anim文件夹(如果没有的话),并在该文件夹下新建一个xml文件,例如:toast_slide_in.xml,其内容如下: ``` <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:duration="500" android:fromXDelta="100%" android:toXDelta="0%" /> </set> ``` 2. 然后在res/drawable目录下创建背景文件,例如:toast_custom_bg.xml,其内容如下: ``` <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="50dp" /> <solid android:color="#FF4081" /> <padding android:bottom="10dp" android:left="12dp" android:right="12dp" android:top="10dp" /> </shape> ``` 3. 然后在Java代码创建一个自定义的Toast,代码如下: ``` public class CustomToast { public static void show(Context context, String message) { LayoutInflater inflater = LayoutInflater.from(context); View layout = inflater.inflate(R.layout.toast_layout, null, false); TextView text = layout.findViewById(R.id.toast_text); ImageView icon = layout.findViewById(R.id.toast_icon); //设置文本和图标 text.setText(message); icon.setImageResource(R.drawable.ic_launcher); Toast toast = new Toast(context); toast.setGravity(Gravity.BOTTOM, 0, 100); toast.setView(layout); toast.setDuration(Toast.LENGTH_LONG); //设置动画 Animation slideIn = AnimationUtils.loadAnimation(context, R.anim.toast_slide_in); toast.getView().startAnimation(slideIn); toast.show(); } } ``` 4. 最后,调用CustomToast.show()方法即可展示自定义的Toast,如下所示: ``` CustomToast.show(this, "这是一条自定义的Toast"); ``` 以上是示例代码,您可以根据需要修改和调整。希望能对您有所帮助!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值