首先产品动画大概长这样:
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.R.id.content来获取Activity的根布局的FrameLayout,无论你设不设置SetContentView都可以拿到ViewGroup
关于LayoutTransition一些介绍,在ViewGroup.addView/removeView的时候,可以将动画带给需要的View。
相关资料
activity界面架构即activity视图层结构
Toast添加动画
LayoutTransition那些事儿