关于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.R.id.content来获取Activity的根布局的FrameLayout,无论你设不设置SetContentView都可以拿到ViewGroup
关于LayoutTransition一些介绍,在ViewGroup.addView/removeView的时候,可以将动画带给需要的View。
相关资料
标签:Toast,动画,java,mParams,WindowManager,Android,android,View 来源: https://blog.csdn.net/android_mylife/article/details/118970165