ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android Activity 功能代码模块化:跳转封装、动画配置与跨端互调实践

Android Activity 功能代码模块化:跳转封装、动画配置与跨端互调实践 简介这份资源面向BPM流程开发人员与Activiti/Camunda学习者聚焦流程运行期的动态控制能力解决部署、动态加签、流程变量传递与指定节点审批人等实际开发问题。压缩包共200个文件约1.57MB以63个java源码、62个class编译文件、56个bpmn流程定义为主辅以13个xml配置、2个properties、2个sql及日志、iml等工程文件覆盖从流程建模到代码实现的完整链路。已有818人学习下载。通过研读这些代码读者可以掌握流程部署的解析与入库方式、运行时动态添加审批节点的实现思路、流程变量的设置与读取技巧以及通过setAssignee、监听器或表达式动态指定任务处理人的做法并借助多个bpmn示例理解分支、子流程与包含节点的编排差异适合需要落地审批流定制功能的中高级开发者参考。1. Activity 功能代码从零散调用到可复用模块的落地路径做过 Android 开发的人大概率都经历过这种场景产品经理丢过来一个需求说“加个活动页”你打开项目一看Activity 的启动代码散落在七八个文件里有的用startActivity硬编码 Intent有的封装了一半的跳转工具类还有的直接在 Adapter 里 new 了一个 Intent。改一个参数要翻遍整个工程这就是 Activity 功能代码没有模块化的典型症状。所谓 Activity 功能代码本质上就是把 Activity 的创建、跳转、参数传递、生命周期管理和动画配置这套链路从“随手写”变成“有章法”的过程。它解决的核心问题是让页面跳转这件事可维护、可追踪、可复用。适合谁看手上有多模块 Android 项目、正在被页面跳转逻辑折磨、或者想给团队定一套跳转规范的工程师。接下来的内容会从 Intent 封装讲到路由表设计再落到动画参数和跨端跳转的实际配置每一步都给可复制的代码和参数说明。2. Activity 跳转代码的三种组织方式与选型依据2.1 裸写 Intent 为什么在超过五个页面后必然失控先看一段最常见的裸写代码// MainActivity.java Intent intent new Intent(MainActivity.this, DetailActivity.class); intent.putExtra(goods_id, goodsId); intent.putExtra(from_page, home_banner); intent.putExtra(track_source, click_001); startActivity(intent);这段代码本身没问题问题是当项目里有三十个 Activity、每个都有三到五个跳转入口时putExtra的 key 全靠字符串硬编码编译器不会帮你检查拼写错误。某天你把goods_id改成了productId接收方还在用getStringExtra(goods_id)运行时拿到 null页面白屏或者崩溃排查半小时起步。这就是典型的“玄学 bug”根因是字符串契约没有约束。裸写 Intent 的另一个问题是参数默认值处理。接收方每次都要写// DetailActivity.java String goodsId getIntent().getStringExtra(goods_id); if (goodsId null) { goodsId ; }每个 Activity 都重复这套逻辑代码量膨胀不说一旦漏了判空空指针就在线上等着你。2.2 静态工厂方法封装小团队性价比最高的方案常见做法是给每个 Activity 加一个静态启动方法把 Intent 的构造和参数写入收敛到一处// DetailActivity.java public class DetailActivity extends AppCompatActivity { private static final String EXTRA_GOODS_ID extra_goods_id; private static final String EXTRA_FROM_PAGE extra_from_page; // 静态工厂方法调用方只关心传什么不关心 key 是什么 public static void start(Context context, String goodsId, String fromPage) { Intent intent new Intent(context, DetailActivity.class); intent.putExtra(EXTRA_GOODS_ID, goodsId); intent.putExtra(EXTRA_FROM_PAGE, fromPage); // 非 Activity 上下文需要加 FLAG_ACTIVITY_NEW_TASK if (!(context instanceof Activity)) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); } Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_detail); String goodsId getIntent().getStringExtra(EXTRA_GOODS_ID); String fromPage getIntent().getStringExtra(EXTRA_FROM_PAGE); // 参数默认值在这里统一处理 if (goodsId null) goodsId ; if (fromPage null) fromPage unknown; loadData(goodsId, fromPage); } }逻辑说明EXTRA_*常量定义在 Activity 内部外部无法直接引用只能通过start()方法传参key 的拼写错误在编译期就被消灭了。参数默认值的处理也收敛到了onCreate里不会散落到各个调用方。参数说明context参数需要判断是否为 Activity 类型因为从 Application 或 Service 启动 Activity 时必须加FLAG_ACTIVITY_NEW_TASK否则会抛AndroidRuntimeException。这个坑在从推送点击、后台服务拉起页面时特别常见。这种方式的边界在于当页面数量超过五十个、且存在多模块比如业务模块之间需要互相跳转时静态工厂方法会导致模块间直接依赖A 模块要跳 B 模块的页面就得 compile B 模块模块解耦就做不到了。这时候需要上路由表。2.3 路由表方案多模块项目的解耦选择路由表的核心思路是每个模块在初始化时把自己的 Activity 注册到一个中央路由表里跳转时只传路由路径和参数不直接引用目标 Activity 的 Class 对象。// 路由注册表简化实现不依赖第三方库 public class RouterTable { private static final MapString, RouteInfo routes new HashMap(); public static void register(String path, Class? extends Activity clazz) { routes.put(path, new RouteInfo(clazz)); } public static void navigate(Context context, String path, Bundle params) { RouteInfo info routes.get(path); if (info null) { // 路由未注册时的兜底打日志 跳转到首页 Log.e(Router, route not found: path); return; } Intent intent new Intent(context, info.clazz); if (params ! null) { intent.putExtras(params); } if (!(context instanceof Activity)) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); } static class RouteInfo { Class? extends Activity clazz; RouteInfo(Class? extends Activity clazz) { this.clazz clazz; } } }逻辑说明register方法在 Application 的onCreate或各模块的初始化回调中调用把路径字符串和 Activity Class 建立映射。navigate方法只依赖路径字符串调用方不需要 import 目标 Activity 的类。参数说明path建议用类似detail/goods的层级命名避免和系统路径冲突。params用 Bundle 传递支持基本类型和 Parcelable。注意 Bundle 里的 key 仍然需要约定建议在路由表里额外维护一份参数 key 的常量定义。选型建议页面少于二十个、单模块项目静态工厂方法足够页面超过三十个、有多模块解耦需求路由表更合适。不要为了“看起来高级”在三个页面的 Demo 里上路由框架那是过度设计。3. Activity 动画设置从 windowAnimation 到共享元素3.1 用 overridePendingTransition 做基础转场Activity 之间的转场动画最基础的配置方式是在startActivity或finish之后调用// 启动 Activity 时指定进入和退出动画 Intent intent new Intent(this, DetailActivity.class); startActivity(intent); overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); // 结束当前 Activity 时指定动画 Override public void finish() { super.finish(); overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right); }逻辑说明overridePendingTransition(enterAnim, exitAnim)的第一个参数是新 Activity 进入时的动画第二个参数是当前 Activity 退出时的动画。必须在startActivity或finish之后立即调用否则动画不生效。参数说明R.anim.slide_in_right这类动画资源定义在res/anim/目录下一个典型的右滑进入动画!-- res/anim/slide_in_right.xml -- translate xmlns:androidhttp://schemas.android.com/apk/res/android android:fromXDelta100% android:toXDelta0 android:duration300 android:interpolatorandroid:anim/decelerate_interpolator /duration建议设在 200 到 400 毫秒之间低于 200 毫秒用户几乎感知不到高于 500 毫秒会显得拖沓。interpolator用decelerate让动画有减速感比线性插值自然。注意从 Android 14API 34开始overridePendingTransition在部分场景下行为有变化如果目标版本较高建议同时关注ActivityOptions的方案。3.2 共享元素动画让页面切换有连续感共享元素动画适合列表页到详情页的图片或标题过渡核心是给两个页面的对应 View 设置相同的transitionName// 列表页启动时指定共享元素 ImageView coverImage holder.cover; coverImage.setTransitionName(cover_ position); Intent intent new Intent(context, DetailActivity.class); intent.putExtra(cover_url, url); ActivityOptionsCompat options ActivityOptionsCompat.makeSceneTransitionAnimation( (Activity) context, coverImage, cover_ position ); ActivityCompat.startActivity((Activity) context, intent, options.toBundle());// 详情页在 onCreate 中设置相同的 transitionName Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 必须在 setContentView 之前调用 getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); setContentView(R.layout.activity_detail); ImageView detailCover findViewById(R.id.detail_cover); detailCover.setTransitionName(getIntent().getStringExtra(transition_name)); }逻辑说明makeSceneTransitionAnimation把列表页的 ImageView 和详情页的 ImageView 建立映射系统会自动计算两个 View 的位置和大小差异生成过渡动画。transitionName必须在两个页面中一致且详情页的设置要在setContentView之后立即执行。参数说明ActivityOptionsCompat来自androidx.core.app包兼容低版本。共享元素动画在返回时也会自动反向播放不需要额外配置。如果动画过程中出现闪烁检查详情页的图片加载是否用了占位图占位图的尺寸和列表页不一致会导致过渡跳变。3.3 动画配置的常见参数对照参数推荐值说明duration250-350ms低于 200ms 感知弱高于 500ms 拖沓interpolatordecelerate / accelerateDecelerate进入用减速退出用加速减速transitionName业务前缀 唯一 ID避免不同列表项之间冲突windowEnterTransitionfade slide组合动画比单一动画自然4. 跨端跳转 ActivityFlutter 与原生互调的代码配置4.1 Flutter 跳转原生 Activity 的 MethodChannel 实现Flutter 项目里跳转原生 Activity标准做法是通过 MethodChannel 通信// Flutter 侧调用原生方法 import package:flutter/services.dart; class NativeBridge { static const MethodChannel _channel MethodChannel(com.example/native); static Futurevoid openNativeActivity(String pageName, Map params) async { try { await _channel.invokeMethod(openActivity, { page: pageName, params: params, }); } on PlatformException catch (e) { print(跳转失败: ${e.message}); } } }// Android 侧在 MainActivity 中注册 MethodChannel public class MainActivity extends FlutterActivity { private static final String CHANNEL com.example/native; Override public void configureFlutterEngine(NonNull FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL) .setMethodCallHandler((call, result) - { if (openActivity.equals(call.method)) { String page call.argument(page); MapString, Object params call.argument(params); // 根据 page 名称映射到具体 Activity if (detail.equals(page)) { Intent intent new Intent(this, DetailActivity.class); if (params ! null) { intent.putExtra(goods_id, (String) params.get(goods_id)); } startActivity(intent); } result.success(null); } else { result.notImplemented(); } }); } }逻辑说明Flutter 侧通过MethodChannel发送方法调用Android 侧在configureFlutterEngine中注册 handler 接收。call.argument取出 Dart 侧传过来的参数映射到 Intent 的 extra。参数说明Channel 名称com.example/native需要 Dart 和 Android 两侧完全一致建议用包名做前缀避免冲突。result.success(null)必须调用否则 Dart 侧的 Future 永远不会完成会造成内存泄漏。4.2 原生跳转 Flutter 页面的参数传递反过来原生 Activity 跳转 Flutter 页面通过FlutterEngine的initialRoute或缓存引擎的navigationChannel// 原生侧跳转到 Flutter 的指定路由 FlutterEngine engine FlutterEngineCache.getInstance().get(main_engine); if (engine ! null) { // 方式一通过 navigationChannel 推送路由 engine.getNavigationChannel().pushRoute(/detail?goodsId123); } else { // 方式二启动新的 FlutterActivity 并指定 initialRoute Intent intent FlutterActivity .withNewEngine() .initialRoute(/detail?goodsId123) .build(this); startActivity(intent); }逻辑说明pushRoute适合已经缓存了 FlutterEngine 的场景不会重建 Flutter 页面性能更好。initialRoute适合冷启动场景会创建新的 Flutter 实例。参数说明路由字符串/detail?goodsId123的格式需要 Flutter 侧的路由表能解析。如果 Flutter 侧用的是go_router或auto_route需要确认路由定义和参数解析规则匹配。缓存引擎的 keymain_engine需要在 Application 初始化时预热。5. Activity 功能代码的避坑与排查记录5.1 后台启动 Activity 被系统拦截现象从 Service 或 BroadcastReceiver 中调用startActivity日志出现abort background activity starts from页面没有弹出。原因Android 10API 29开始后台应用启动 Activity 受到严格限制除非满足特定条件如拥有 SYSTEM_ALERT_WINDOW 权限、前台服务等。解决优先用通知Notification引导用户点击通知的 PendingIntent 启动 Activity 不受此限制。如果确实需要后台弹出检查是否在前台服务中调用并确认AndroidManifest.xml中声明了FOREGROUND_SERVICE权限。不要试图用反射绕过限制高版本系统会直接抛异常。5.2 共享元素动画返回时闪烁现象列表页到详情页的共享元素动画正常但按返回键时图片先消失再出现有明显闪烁。原因详情页的 ImageView 在onCreate中设置了transitionName但图片是异步加载的返回时图片还没加载完或者尺寸和列表页不一致。解决在详情页的图片加载回调中等图片加载完成后再设置transitionName或者用postponeEnterTransition()延迟转场图片加载完成后调用startPostponedEnterTransition()。5.3 MethodChannel 调用后 Future 不完成现象Flutter 侧await _channel.invokeMethod(...)一直挂起后续代码不执行。原因Android 侧的setMethodCallHandler中某个分支没有调用result.success()或result.error()Dart 侧的 Future 永远处于 pending 状态。解决确保 handler 的每个分支都有返回值。用 try-catch 包裹业务逻辑catch 中调用result.error()。可以在 Dart 侧加超时await _channel.invokeMethod(...).timeout(Duration(seconds: 5))。5.4 overridePendingTransition 不生效现象调用了overridePendingTransition但页面切换没有动画。原因一是在startActivity之前调用了顺序反了二是目标 Activity 在AndroidManifest.xml中设置了android:theme为android:style/Theme.NoDisplay或类似无动画主题三是系统开发者选项里关闭了动画。解决确认调用顺序是startActivity然后立即overridePendingTransition。检查目标 Activity 的主题是否继承了带windowAnimationStyle的样式。在测试机上检查“开发者选项 → 动画时长”是否被设为了 0.5x 或关闭。5.5 路由表注册时机导致的空指针现象App 冷启动后第一次跳转某个页面路由找不到日志打印route not found。原因路由注册代码放在了某个模块的onCreate里但该模块在冷启动时没有初始化或者初始化顺序在跳转之后。解决把所有路由注册收敛到 Application 的onCreate中或者用懒加载 同步初始化的方式。如果模块太多导致启动慢可以按需初始化但要在跳转前确保目标模块已注册。加一个兜底逻辑路由找不到时跳转到首页并上报日志避免用户卡在空白页。6. 用一张路由表把 Activity 功能代码串起来前面几章分别讲了跳转封装、动画配置、跨端互调和避坑最后落到一个具体技巧用一张统一的路由表把 Activity 的跳转、参数、动画配置全部管起来。思路是在路由注册时不仅注册 Class还把转场动画和参数 key 一起注册进去。// 扩展路由表注册时带上动画和参数定义 public class RouteTable { private static final MapString, RouteEntry entries new HashMap(); public static void register(String path, Class? extends Activity clazz, int enterAnim, int exitAnim, String[] requiredParams) { entries.put(path, new RouteEntry(clazz, enterAnim, exitAnim, requiredParams)); } public static void navigate(Activity activity, String path, Bundle params) { RouteEntry entry entries.get(path); if (entry null) { Log.e(Route, not found: path); return; } // 参数校验缺少必传参数时直接报错避免运行时崩溃 if (entry.requiredParams ! null params ! null) { for (String key : entry.requiredParams) { if (!params.containsKey(key)) { throw new IllegalArgumentException(missing param: key for route: path); } } } Intent intent new Intent(activity, entry.clazz); if (params ! null) intent.putExtras(params); activity.startActivity(intent); activity.overridePendingTransition(entry.enterAnim, entry.exitAnim); } static class RouteEntry { Class? extends Activity clazz; int enterAnim, exitAnim; String[] requiredParams; RouteEntry(Class? extends Activity clazz, int enterAnim, int exitAnim, String[] requiredParams) { this.clazz clazz; this.enterAnim enterAnim; this.exitAnim exitAnim; this.requiredParams requiredParams; } } }注册时这样写// Application.onCreate 中统一注册 RouteTable.register(detail/goods, DetailActivity.class, R.anim.slide_in_right, R.anim.slide_out_left, new String[]{goods_id}); RouteTable.register(settings/profile, ProfileActivity.class, R.anim.fade_in, R.anim.fade_out, null);逻辑说明requiredParams在跳转时做前置校验缺少必传参数直接抛异常把运行时崩溃提前到开发阶段暴露。动画配置跟着路由走调用方不需要每次手动写overridePendingTransition。参数说明enterAnim和exitAnim传 0 表示使用系统默认动画。requiredParams传 null 表示无必传参数。这套方案不依赖任何第三方路由库代码量在两百行以内适合中小项目直接落地。我自己的习惯是每加一个新 Activity先在路由表里注册一行再去写 Activity 本身的代码。这样跳转入口永远只有一处改参数、改动画、查调用方都在一个文件里完成。血泪经验是千万别等到页面堆到五十个再回头补路由表那时候改造成本至少翻三倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表