一 窗口过渡动画
1.1 案例效果图

1.2 案例源码
1.2.1 添加权限 (AndroidManifest.xml)
xml
<!-- 系统悬浮窗权限(Android 6.0+需动态请求) -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
1.2.2 窗口显示动画 (win_anim_enter.xml)
xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="1000"
android:fromXScale="0.2"
android:fromYScale="0.2"
android:toXScale="1"
android:toYScale="1" />
<alpha
android:duration="1000"
android:fromAlpha="0.2"
android:toAlpha="1" />
</set>
1.2.3 窗口移除动画 (win_anim_exit.xml)
xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="1000"
android:fromXScale="0.2"
android:fromYScale="0.2"
android:toXScale="1"
android:toYScale="1" />
<alpha
android:duration="1000"
android:fromAlpha="0.2"
android:toAlpha="1" />
</set>
1.2.4 动画样式 (themes.xml)
xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="floatAnimStyle">
<item name="android:windowEnterAnimation">@anim/win_anim_enter</item>
<item name="android:windowExitAnimation">@anim/win_anim_exit</item>
</style>
</resources>
1.2.5 布局文件 (activity_main.xml)
xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加窗口"/>
<Button
android:id="@+id/btnRemove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="移除窗口"/>
</LinearLayout>
1.2.6 悬浮窗布局 (float_layout.xml)
xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/floatingView"
android:layout_width="200dp"
android:layout_height="100dp"
android:background="#77000000"
android:text="悬浮窗口"
android:textColor="#FF0000"
android:gravity="center"/>
1.2.7 MainActivity 实现
java
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_OVERLAY_PERMISSION = 100;
private WindowManager mWindowManager;
private View mFloatView;
private WindowManager.LayoutParams layoutParams;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化mWindowManager
mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
findViewById(R.id.btnAdd).setOnClickListener(v -> addFloatingWindow());
findViewById(R.id.btnRemove).setOnClickListener(v -> removeFloatingWindow());
}
private void addFloatingWindow() {
// 检查悬浮窗权限
if (!Settings.canDrawOverlays(this)) {
requestOverlayPermission();
return;
}
if (mFloatView != null) return; // 防止重复添加
// 创建悬浮窗视图
mFloatView = LayoutInflater.from(this).inflate(R.layout.float_layout, null);
mFloatView.setOnTouchListener(new ViewTouchListener());
// 设置窗口参数
int type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT
, WindowManager.LayoutParams.WRAP_CONTENT, type
, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT);
// 初始位置
layoutParams.gravity = Gravity.TOP | Gravity.START;
layoutParams.x = 100;
layoutParams.y = 300;
layoutParams.windowAnimations = R.style.floatAnimStyle; //窗口过渡动画
try {
mWindowManager.addView(mFloatView, layoutParams);
} catch (Exception e) {
Toast.makeText(this, "添加失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private void removeFloatingWindow() {
if (mFloatView != null) {
try {
mWindowManager.removeView(mFloatView);
mFloatView = null;
} catch (Exception e) {
Toast.makeText(this, "移除失败", Toast.LENGTH_SHORT).show();
}
}
}
// 处理拖拽的触摸监听器
private class ViewTouchListener implements View.OnTouchListener {
private int initialX, initialY;
private float initialTouchX, initialTouchY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = layoutParams.x;
initialY = layoutParams.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
return true;
case MotionEvent.ACTION_MOVE:
layoutParams.x = initialX + (int) (event.getRawX() - initialTouchX);
layoutParams.y = initialY + (int) (event.getRawY() - initialTouchY);
mWindowManager.updateViewLayout(mFloatView, layoutParams);
return true;
}
return false;
}
}
// 请求悬浮窗权限
private void requestOverlayPermission() {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_OVERLAY_PERMISSION);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_OVERLAY_PERMISSION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) {
addFloatingWindow();
}
}
}
@Override
protected void onDestroy() {
removeFloatingWindow(); // 防止Activity销毁后窗口残留
super.onDestroy();
}
}
二 过渡动画层级图分析
2.1 使用winscope抓取SurfaceFlinger图层信息
抓取图层信息。
bash
adb shell su root service call SurfaceFlinger 1025 i32 1 #开始录制
adb shell su root service call SurfaceFlinger 1025 i32 0 #停止录制
adb pull /data/misc/wmtrace . //获取捕获的SurfaceFlinger图层信
使用adb指令将抓取的SurfaceFlinger图层信息导出来。
2.2 使用winscope进行分析
2.2.1 窗口未被添加到WMS中

层级树的叶子节点Leaf:3:14#0还看不到我们的窗口节点。
2.2.2 窗口被添加到WMS中

可以在层级树的叶子节点Leaf:3:14#0看到新加的Sys2038窗口图层信息。
2.2.3 窗口显示过渡动画开始

xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="1000"
android:fromXScale="0.2"
android:fromYScale="0.2"
android:toXScale="1"
android:toYScale="1" />
<alpha
android:duration="1000"
android:fromAlpha="0.2"
android:toAlpha="1" />
</set>
多了一个含有animation-leash关键字的Surface图层,bounds接近我们在动画样式文件win_anim_enter.xml里面设置的初始值0.2倍,透明度同样接近我们设置的初始值0.2。
2.2.4 窗口显示过渡动画即将结束

bound接近1.0,透明度也接近1.0,过渡动画即将执行完毕。
2.2.5 窗口显示过渡动画结束

animation-leash关键字的Surface图层被移除,只剩下我们要添加的类型为Sys2038的窗口图层。
2.2.6 窗口准备被移除,开始执行过渡动画

xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="1000"
android:fromXScale="1"
android:fromYScale="1"
android:toXScale="0.2"
android:toYScale="0.2" />
<alpha
android:duration="1000"
android:fromAlpha="1"
android:toAlpha="0.2" />
</set>
多了一个含有animation-leash关键字的Surface图层,bounds是我们在动画样式文件win_anim_exit.xml里面设置的初始值1.0倍,透明度同样是我们设置的初始值1.0。
2.2.7 窗口被移除过渡动画即将结束

bound接近0.2,透明度也接近0.2,过渡动画即将执行完毕。
2.2.8 窗口被移除过渡动画结束

animation-leash关键字的Surface图层被移除,类型为Sys2038的之前添加的窗口图层也被移除,层级树的叶子节点Leaf:3:14#0恢复到了原来的样式。
2.3 窗口过渡动画期间的图层信息变化
