当前位置:首页 > 技术 > 正文内容

Android 折叠动画深入实践:FoldableLayout 使用指南

访客 技术 2026年7月3日 1

引言

在 Android 应用开发中,视图间的过渡动画往往决定了用户体验的优劣。许多开发者试图实现类似纸张折叠的 3D 过渡效果,但常因复杂的矩阵运算和状态管理而放弃。FoldableLayout 是一个轻量级开源库,仅 31KB,却能实现接近 Material Design 3 标准的折叠动画。本文将从基础集成到高级定制,详细讲解如何利用该库提升应用的交互质量。

项目概览

FoldableLayout 专注于视图的立体折叠过渡,核心性能指标如下:

特性指标对比自定义方案
包体积31KB减少约 80% 代码量
最低 API14覆盖 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 视图变换原理。

标签: FoldableLayout

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。