Android 折叠动画深入实践:FoldableLayout 使用指南
引言
在 Android 应用开发中,视图间的过渡动画往往决定了用户体验的优劣。许多开发者试图实现类似纸张折叠的 3D 过渡效果,但常因复杂的矩阵运算和状态管理而放弃。FoldableLayout 是一个轻量级开源库,仅 31KB,却能实现接近 Material Design 3 标准的折叠动画。本文将从基础集成到高级定制,详细讲解如何利用该库提升应用的交互质量。
项目概览
FoldableLayout 专注于视图的立体折叠过渡,核心性能指标如下:
| 特性 | 指标 | 对比自定义方案 |
|---|---|---|
| 包体积 | 31KB | 减少约 80% 代码量 |
| 最低 API | 14 | 覆盖 99.5% 设备 |
| 帧率 | 60fps | 硬件加速,无掉帧 |
| 内存占用 | <5MB | 视图复用机制保障 |
项目结构如下:
FoldableLayout/
├── library/ # 核心库
│ └── src/main/java/com/alexvasilkov/foldablelayout/
│ ├── FoldableItemLayout.java # 单个折叠项容器
│ ├── FoldableListLayout.java # 折叠列表容器
│ ├── UnfoldableView.java # 详情展开容器
│ ├── Utils.java # 工具类
│ └── shading/ # 阴影效果
│ ├── FoldShading.java # 阴影接口
│ ├── GlanceFoldShading.java # 预览图阴影
│ └── SimpleFoldShading.java # 简单阴影
└── sample/ # 示例应用
└── src/main/
├── java/ # 示例 Activity
└── res/layout/ # 布局文件
核心组件详解
1. FoldableListLayout:垂直折叠列表
该组件基于回收复用机制,最多同时维护三个可见项(前、中、后)以优化性能。
// 动画持续时间(每项)
private static final long ANIM_DURATION_PER_ITEM = 600L;
// 最小滑动触发速度
private static final float MIN_FLING_VELOCITY = 600f;
// 滚动灵敏度因子
private static final float DEFAULT_SCROLL_FACTOR = 1.33f;
其状态机包含展开、折叠、动画中、闲置四种状态,通过 setFoldShading() 可自定义阴影。
2. UnfoldableView:详情展开视图
用于实现从列表项到详情页的平滑过渡,核心在于坐标转换。当旋转角从 0° 增加到 180° 时,视图中心从封面向详情位置移动:
float progress = rotation / 180f; // 0 → 1
float srcX = coverViewPosition.centerX();
float dstX = detailViewPosition.centerX();
float srcY = coverViewPosition.top;
float dstY = detailViewPosition.centerY();
setTranslationX((srcX - dstX) * (1f - progress));
setTranslationY((srcY - dstY) * (1f - progress));
快速集成指南
环境配置
在 build.gradle 中添加:
dependencies {
implementation 'com.alexvasilkov:foldable-layout:1.2.1'
}
在 AndroidManifest.xml 中启用硬件加速:
<application android:hardwareAccelerated="true" ... >
实现折叠列表(3 步)
Step 1: 布局文件
<com.alexvasilkov.foldablelayout.FoldableListLayout
android:id="@+id/foldable_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Step 2: 列表项布局 (list_item.xml)
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/item_image"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="18sp"/>
</LinearLayout>
Step 3: Activity 初始化
public class SampleListActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sample_list);
FoldableListLayout foldableList = findViewById(R.id.foldable_list);
foldableList.setFoldShading(new SimpleFoldShading()); // 可选
foldableList.setAdapter(new MyListAdapter());
}
}
实现详情展开效果(5 步)
Step 1: 主布局结构
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<View
android:id="@+id/touch_interceptor"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"/>
<LinearLayout
android:id="@+id/details_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="invisible" />
<com.alexvasilkov.foldablelayout.UnfoldableView
android:id="@+id/unfoldable_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge>
Step 2: 初始化 UnfoldableView
public class DetailActivity extends AppCompatActivity {
private UnfoldableView unfoldView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
unfoldView = findViewById(R.id.unfoldable_view);
Bitmap glance = BitmapFactory.decodeResource(getResources(), R.drawable.glance);
unfoldView.setFoldShading(new GlanceFoldShading(glance));
unfoldView.setOnFoldingListener(new UnfoldableView.SimpleFoldingListener() {
@Override
public void onUnfolding(UnfoldableView v) {
findViewById(R.id.details_layout).setVisibility(View.VISIBLE);
}
@Override
public void onFoldedBack(UnfoldableView v) {
findViewById(R.id.details_layout).setVisibility(View.INVISIBLE);
}
});
}
}
Step 3: 列表项点击处理
public void openDetail(View coverView, ItemData data) {
ImageView img = findViewById(R.id.detail_image);
TextView title = findViewById(R.id.detail_title);
img.setImageResource(data.getImageId());
title.setText(data.getTitle());
unfoldView.unfold(coverView, findViewById(R.id.details_layout));
}
Step 4: 返回键控制
@Override
public void onBackPressed() {
if (unfoldView != null && (unfoldView.isUnfolded() || unfoldView.isUnfolding())) {
unfoldView.foldBack();
} else {
super.onBackPressed();
}
}
Step 5: 适配器绑定
public class MyAdapter extends BaseAdapter {
@Override
public View getView(int pos, View convertView, ViewGroup parent) {
ViewHolder h = ...;
h.image.setOnClickListener(v -> {
((DetailActivity) context).openDetail(v, getItem(pos));
});
return convertView;
}
}
高级定制
阴影效果定制
库提供三种阴影,通过 setFoldShading() 设置:
| 类型 | 特性 | 适用场景 |
|---|---|---|
| SimpleFoldShading | 纯色渐变 | 性能优先 |
| GlanceFoldShading | 带预览图 | 列表到详情过渡 |
| 自定义 | 完全控制绘制 | 特殊视觉效果 |
自定义示例:
public class MyCustomShading implements FoldShading {
private final Paint paint = new Paint();
public MyCustomShading() {
paint.setColor(0xAA444444);
paint.setStyle(Paint.Style.FILL);
}
@Override
public void onPreDraw(Canvas c, Rect bounds, float rotation, int gravity) { }
@Override
public void onPostDraw(Canvas c, Rect bounds, float rotation, int gravity) {
float alpha = Math.abs(rotation) / 90f;
paint.setAlpha((int) (alpha * 255));
c.drawRect(bounds, paint);
}
}
动画参数调整
// 调整滚动灵敏度
foldableList.setScrollFactor(1.5f);
// UnfoldableView 配置
unfoldView.setAutoScaleEnabled(true); // 自动缩放适应屏幕
性能优化
- 复用 convertView
- 使用 Glide 加载图片并压缩
- 减少重叠视图数量
- 避免在动画中执行复杂逻辑
if (unfoldView.isAnimating()) return; // 滑动时跳过计算
实战场景
图片画廊
从缩略图过渡到高清图,保持宽高比,使用低分辨率预览图优化性能:
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 4; // 1/4 分辨率
Bitmap glance = BitmapFactory.decodeResource(getResources(), R.drawable.glance, opts);
卡片翻转
使用 FoldableItemLayout 承载正反两面:
<com.alexvasilkov.foldablelayout.FoldableItemLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout android:id="@+id/front_side">
<!-- 正面内容 -->
</LinearLayout>
<LinearLayout android:id="@+id/back_side">
<!-- 背面内容 -->
</LinearLayout>
</com.alexvasilkov.foldablelayout.FoldableItemLayout>
常见问题
Q1: 视图闪烁
A: 启用硬件加速,父容器设置 android:background="@null".
Q2: 触摸事件丢失
A: 在展开时拦截列表触摸:
@Override
public void onUnfolding(UnfoldableView v) {
touchInterceptor.setClickable(true);
}
@Override
public void onUnfolded(UnfoldableView v) {
touchInterceptor.setClickable(false);
}
Q3: 屏幕适配问题
A: 使用 view.getGlobalVisibleRect(rect) 动态获取位置。
Q4: 与 RecyclerView 冲突
A: 使用 FrameLayout 隔离:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 放入 FoldableListLayout -->
</FrameLayout>
总结
FoldableLayout 通过矩阵变换和状态管理,以极小的体积实现高质量折叠动画,简化了复杂过渡的实现过程。其灵活定制选项和良好性能使其适用于多种场景。未来可期待的改进包括水平折叠、更多插值器、Jetpack Compose 支持等。掌握该库不仅能提升应用交互,也有助于深入理解 Android 视图变换原理。