├── cc++ ├── c++学习——3.类.md ├── c++学习——2.容器算法.md ├── c++学习——5.高级主题.md ├── c++学习——4.面向对象及泛型.md ├── c++学习——1.基础语言.md └── c学习——基础.md ├── android ├── Android学习——VAP源码.md ├── Android实战——系统悬浮窗踩坑记.md ├── Android实战——Cocos游戏容器搭建篇.md ├── Android实战——RecyclerView条目曝光埋点.md ├── Android学习——Handler通信机制.md ├── Android实战——Cocos游戏容器多进程通信.md └── AndroidJNI实战——记录实现视频播放器.md ├── flutter ├── Flutter上线项目实战——图片视频预览.md ├── Flutter上线项目实战——Vap视频动画.md ├── Flutter上线项目实战——环信客服插件.md ├── Flutter上线项目实战——防止录屏.md ├── Flutter上线项目实战——即时通讯Protobuf.md ├── Flutter上线项目实战——队列任务.md ├── Flutter上线项目实战——性能优化篇(未完待续).md ├── Flutter上线项目实战——即时通讯端对端加密(E2E).md ├── Flutter上线项目实战——腾讯点播直播下载.md ├── Flutter上线项目实战——苹果内购.md └── Flutter上线项目实战——路由篇.md ├── README.md └── common └── Android&RN&Flutter实战——防抖节流函数.md /cc++/c++学习——3.类.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cc++/c++学习——2.容器算法.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cc++/c++学习——5.高级主题.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cc++/c++学习——4.面向对象及泛型.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cc++/c++学习——1.基础语言.md: -------------------------------------------------------------------------------- 1 | ###### 一、typedef定义类型 2 | 3 | ```c++ 4 | typedef int num; 5 | num a = 1; 6 | ``` 7 | 8 | ###### 二、枚举 9 | 10 | ```c++ 11 | enum Gender {male = 1, female}; 12 | std::cout << female << std::endl; // 2 13 | ``` 14 | 15 | ###### 三、类类型 16 | 17 | 用class和struct关键字定义唯一区别在于默认访问级别:struct的成员默认为public,而class的成员为private 18 | 19 | ```c++ 20 | class Person { // 等价于struct 21 | public: 22 | std::string name; 23 | private: 24 | std::string address; 25 | double height; 26 | int age; 27 | 28 | }; 29 | ``` 30 | 31 | ###### 四、头文件用于声明非定义 32 | 33 | ```c++ 34 | extern int ix = 1; //定义 35 | int iy; //定义 36 | extern int iz;//声明 37 | extern const int &ri; //声明 38 | 39 | //extern一般在头文件中声明,以便其他文件能共享 40 | ``` 41 | 42 | ###### 五、Vector初试 43 | 44 | ```c++ 45 | //初始化 46 | vector nums = {2,7,11,15}; 47 | //vector参数 48 | vector res = twoNum(nums, 9); 49 | vector twoNum(vector& nums, int target) {} 50 | //遍历vector 51 | for (vector::size_type i = 0; i != nums.size(); i++) { 52 | std::cout << nums[i] << std::endl; 53 | } 54 | //用迭代器便利vector 55 | for (vector::iterator iter = nums.begin(); iter != nums.end(); iter++) { 56 | std::cout << *iter << std::endl; 57 | } 58 | //添加元素 59 | nums.push_back(20); 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /android/Android学习——VAP源码.md: -------------------------------------------------------------------------------- 1 | ### 一、背景介绍 2 | ##### 1. VAP(Video Animation Player)是直播中台使用的一个视频动画特效SDK,可以通过制作Alpha通道分离的视频素材,再在客户端上通过OpenGL ES重新实现Alpha通道和RGB通道的混合,从而实现在端上播放带透明通道的视频。 3 | > 已经接入的app 4 | 5 | ![](http://file.jinxianyun.com/vap_apps.png) 6 | 7 | > 同原理实现也用在其他app 8 | 9 | 抖音、西瓜视频、今日头条、爱奇艺、比心等 10 | 11 | 12 | ##### 2. 方案对比 13 | 目前较常见的动画实现方案有帧动画、gif/webp、lottie/SVGA,对于复杂动画特效的实现做个简单对比 14 | 15 | 方案 | 文件大小 | 解码方式 | 特效支持 | 应用表现 16 | ---|---|---|---|--- 17 | Lottie/SVGA | 无法导出 | 软解 | 部分复杂特效不支持 | 绘制耗时,内存抖动 18 | gif/webp | 4.6M | 软解 | 只支持8位色彩 | 文件资源消耗大 19 | apng | 10.6M | 软解 | 全支持 | 资源消耗大 20 | vap | 1.5M | 硬解码 | 全支持 | 性能好,复杂动画支持好 21 | 22 | ##### 3. 运行效果 23 | ![image](http://file.jinxianyun.com/flutter_vap.gif) 24 | 25 | 26 | ##### 4. 本文主要内容 27 | 28 | 主要介绍VAP如何实现,从MediaCodec视频解码到OpenGL ES 渲染RGB及Alpha通道,最后输出到TextureView上。 29 | 30 | 31 | 32 | ### 二、VAP实现架构 33 | ##### 1. 需要的技术 34 | - OpenGL ES: 创建纹理,及绘制工作 35 | - TextureView: Android UI组件,持有SurfaceTexture,监听SurfaceTexture的size变化,做显示区域更新 36 | - SurfaceTexture: 持有OpenGL ES创建的纹理,监听解码后的帧数据做帧更新,并通知OpenGL ES绘制 37 | - MediaCodec: 解码,把解码后的数据绑定到SurfaceTexture 38 | 39 | ##### 2. 实现流程图 40 | ![image](http://file.jinxianyun.com/vap_main.jpg) 41 | [image](http://file.jinxianyun.com/vap_main.jpg) 42 | 43 | 44 | ##### 3. 整体工作流程 45 | 为了方便查看各个类的职责及顺序执行的流程,这里用泳道图: 46 | ![image](http://file.jinxianyun.com/vap_codes.jpg) 47 | [image](http://file.jinxianyun.com/vap_codes.jpg) 48 | 49 | 50 | ### 三、其他 51 | - [本文源码](https://github.com/Tencent/vap) 52 | - [字节对应实现](https://github.com/bytedance/AlphaPlayer) 53 | - [flutter版vap](https://github.com/qq326646683/flutter_vap) 54 | 55 | 56 | --- 57 | 完结,撒花🎉 -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——图片视频预览.md: -------------------------------------------------------------------------------- 1 | # interactiveviewer_gallery 2 | [![pub package](https://img.shields.io/pub/v/interactiveviewer_gallery.svg)](https://pub.dartlang.org/packages/interactiveviewer_gallery) 3 | 4 | 图片预览&视频预览&图片视频混合预览的容器UI 5 | 1. 支持双指缩放 6 | 2. 支持双击放大 7 | 3. 支持左右切换图片 8 | 4. 支持下拉手势返回, 伴随缩小、移动、透明度变化 9 | 5. 支持视频失去焦点自动暂停 10 | 11 | ## 预览 12 | [qiniu](http://file.jinxianyun.com/interactiveviewer_gallery_0_1_0.mp4)/[youtube](https://youtu.be/S-93Et_nYQs) 13 | 14 | 15 | [apk download](http://file.jinxianyun.com/interactiveviewer_gallery_0_1_0.apk) 16 | 17 | ## 安装 18 | 19 | 因为该库是在InteractiveViewer基础上实现的, 所以flutter版本不低于1.20.0 20 | ```dart 21 | interactiveviewer_gallery: ${last_version} 22 | ``` 23 | 24 | ## 如何使用 25 | 1. 九宫格图片页面中图片组件包裹Hero(用来跳转的承接动画) 26 | ```dart 27 | Hero( 28 | tag: source.url, 29 | child: ${gridview item} 30 | ) 31 | ``` 32 | 33 | 2. 点击九宫格图片跳转到图片预览页面 34 | ```dart 35 | Navigator.of(context).push( 36 | HeroDialogRoute( 37 | builder: (BuildContext context) => InteractiveviewerGallery( 38 | sources: sourceList, 39 | initIndex: sourceList.indexOf(source), 40 | // 定义自己的item 41 | itemBuilder: itemBuilder, 42 | onPageChanged: (int pageIndex) { 43 | print("nell-pageIndex:$pageIndex"); 44 | }, 45 | ), 46 | ), 47 | ); 48 | ``` 49 | 50 | 3. 定义自己的item (因为每个人的UI设计不一样, 所以这里需要自己实现item, 该库只是一个UI容器), 可以参考预览视频中的实现: [example/lib/main.dart](https://github.com/qq326646683/interactiveviewer_gallery/blob/main/example/lib/main.dart) 51 | 52 | ```dart 53 | Widget itemBuilder(BuildContext context, int index, bool isFocus) { 54 | DemoSourceEntity sourceEntity = sourceList[index]; 55 | if (sourceEntity.type == 'video') { 56 | return DemoVideoItem( 57 | sourceEntity, 58 | isFocus: isFocus, 59 | ); 60 | } else { 61 | return DemoImageItem(sourceEntity); 62 | } 63 | } 64 | ``` 65 | 66 | ## 其他 67 | 欢迎pr和讨论 68 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——Vap视频动画.md: -------------------------------------------------------------------------------- 1 | ### 背景 2 | 透明视频动画是目前比较流行的实现动画的一种, 大厂也相继开源自己的框架,最终我们选中[腾讯vap](https://github.com/Tencent/vap),它支持了Android、IOS、Web,为我们封装flutter_vap提供了天然的便利,并且它提供了将帧图片生成带alpha通道视频的工具,这简直太赞了。 3 | 4 | 5 | VAP(Video Animation Player)是企鹅电竞开发,用于播放酷炫动画的实现方案。 6 | - 相比Webp, Apng动图方案,具有高压缩率(素材更小)、硬件解码(解码更快)的优点 7 | - 相比Lottie,能实现更复杂的动画效果(比如粒子特效) 8 | 9 | ### 预览 10 | ![image](http://file.jinxianyun.com/flutter_vap.gif) 11 | 12 | [video for youtube](https://youtu.be/OCLkFhcYqwA) 13 | 14 | [video for qiniu](http://file.jinxianyun.com/flutter_vap.mp4) 15 | 16 | [apk download](http://file.jinxianyun.com/flutter_vap.apk) 17 | 18 | ### 安装 19 | ``` 20 | flutter_vap: ${last_version} 21 | ``` 22 | 23 | ### 使用 24 | 25 | 1. 播放本地视频 26 | ```dart 27 | import 'package:flutter_vap/flutter_vap.dart'; 28 | 29 | /// return: play error: {"status": "failure", "errorMsg": ""} 30 | /// play complete: {"status": "complete"} 31 | Future> _playFile(String path) async { 32 | if (path == null) { 33 | return null; 34 | } 35 | var res = await VapController.playPath(path); 36 | if (res["status"] == "failure") { 37 | showToast(res["errorMsg"]); 38 | } 39 | return res; 40 | } 41 | ``` 42 | 43 | 2. 播放asset视频 44 | ```dart 45 | Future> _playAsset(String asset) async { 46 | if (asset == null) { 47 | return null; 48 | } 49 | var res = await VapController.playAsset(asset); 50 | if (res["status"] == "failure") { 51 | showToast(res["errorMsg"]); 52 | } 53 | return res; 54 | } 55 | ``` 56 | 57 | 3. 停止播放 58 | ```dart 59 | VapController.stop() 60 | ``` 61 | 62 | 4. 队列播放 63 | ```dart 64 | _queuePlay() async { 65 | // 模拟多个地方同时调用播放,使得队列执行播放。 66 | QueueUtil.get("vapQueue").addTask(() => VapController.playPath(downloadPathList[0])); 67 | QueueUtil.get("vapQueue").addTask(() => VapController.playPath(downloadPathList[1])); 68 | } 69 | ``` 70 | 71 | 5. 取消队列播放 72 | ```dart 73 | QueueUtil.get("vapQueue").cancelTask(); 74 | ``` 75 | 76 | ### 例子 77 | [github](https://github.com/qq326646683/flutter_vap/blob/main/example/lib/main.dart) 78 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——环信客服插件.md: -------------------------------------------------------------------------------- 1 | # 2 | ## 1.Describe 3 | 4 | 1.封装的环信客服的功能: 初始化、注册、登录、进入会话; 5 | 6 | 2.绘画页面easeUI里包含的同kefu-android-demo、kefu-ios-demo一样的功能: 7 | 8 | android: 选图片、拍照片、选视频、发文件、发语音、文字、表情 9 | 10 | ios: 选照片、拍照片、拍视频、发定位、发语音、文字、表情 11 | 12 | 3.语音、视频通话尝试均不可用 13 | 14 | ## 2.Setup 15 | ``` 16 | // 环信自带的ui 17 | flutter_easemob_kefu: ${last_version} 18 | 19 | or 20 | 21 | flutter_easemob_kefu: 22 | git: 23 | url: https://github.com/qq326646683/flutter_easemob_kefu.git 24 | 25 | // 自定义ui(根据自己的ui,去修改原生两端的ui代码) 26 | flutter_easemob_kefu: 27 | git: 28 | url: https://github.com/qq326646683/flutter_easemob_kefu.git#custom-ui 29 | 30 | ``` 31 | 32 | > For Ios: 33 | 34 | 相册、相机等权限配置到plist 35 | 36 | ## 3.Usages 37 | 38 | ``` 39 | /// 初始化 40 | /// appKey: “管理员模式 > 渠道管理 > 手机APP”页面的关联的“AppKey” 41 | /// tenantId: “管理员模式 > 设置 > 企业信息”页面的“租户ID” 42 | static void init(String appKey, String tenantId) { 43 | _channel.invokeMethod("init", { 44 | "appKey": appKey, 45 | "tenantId": tenantId, 46 | }); 47 | } 48 | 49 | /// 注册 50 | static Future register(String username, String password) async { 51 | return _channel.invokeMethod("register", { 52 | "username": username, 53 | "password": password, 54 | }); 55 | } 56 | 57 | /// 登录 58 | static Future login(String username, String password) async { 59 | return _channel.invokeMethod("login", { 60 | "username": username, 61 | "password": password, 62 | }); 63 | } 64 | 65 | /// 是否登录 66 | static Future get isLogin { 67 | return _channel.invokeMethod("isLogin"); 68 | } 69 | 70 | /// 注销登录 71 | static Future logout() async { 72 | return _channel.invokeMethod("logout"); 73 | } 74 | 75 | /// 会话页面: 76 | /// imNumber: “管理员模式 > 渠道管理 > 手机APP”页面的关联的“IM服务号” 77 | static void jumpToPage(String imNumber) { 78 | _channel.invokeMethod("jumpToPage", { 79 | "imNumber": imNumber, 80 | }); 81 | } 82 | ``` 83 | 84 | ## 4.Example 85 | ``` 86 | FlutterEasemobKefu.init("1439201009092337#kefuchannelapp86399", "86399"); 87 | 88 | bool isSuccess = await FlutterEasemobKefu.register("nell", "123456"); 89 | 90 | bool isSuccess = await FlutterEasemobKefu.login("nell", "123456"); 91 | 92 | bool isLogin = await FlutterEasemobKefu.isLogin; 93 | if (isLogin) { 94 | FlutterEasemobKefu.jumpToPage("kefuchannelimid_316626"); 95 | } 96 | ``` 97 | 98 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——防止录屏.md: -------------------------------------------------------------------------------- 1 | ## 1.Setup 2 | 3 | ``` 4 | flutter_forbidshot: 0.0.1 5 | ``` 6 | 7 | ## 2.Usage 8 | 9 | > IOS API 10 | 11 | 1. Get the current recording screen state (获取到当前是否在录屏) 12 | ``` 13 | bool isCapture = await FlutterForbidshot.iosIsCaptured; 14 | ``` 15 | 2. Screen recording status changes will call back (录屏状态变化会回调) 16 | ``` 17 | StreamSubscription subscription = FlutterForbidshot.iosShotChange.listen((event) {}); 18 | ``` 19 | 20 | > Android API 21 | 22 | 1. Turn on the forbid screen (开启禁止录屏) 23 | ``` 24 | FlutterForbidshot.setAndroidForbidOn(); 25 | ``` 26 | 2. Turn off the forbid screen (取消禁止录屏) 27 | ``` 28 | FlutterForbidshot.setAndroidForbidOff(); 29 | ``` 30 | 31 | 32 | ## 3.Example 33 | ``` dart 34 | class _MyAppState extends State { 35 | bool isCaptured = false; 36 | StreamSubscription subscription; 37 | 38 | @override 39 | void initState() { 40 | super.initState(); 41 | init(); 42 | } 43 | 44 | init() async { 45 | bool isCapture = await FlutterForbidshot.iosIsCaptured; 46 | setState(() { 47 | isCaptured = isCapture; 48 | }); 49 | subscription = FlutterForbidshot.iosShotChange.listen((event) { 50 | setState(() { 51 | isCaptured = !isCaptured; 52 | }); 53 | }); 54 | 55 | } 56 | 57 | @override 58 | void dispose() { 59 | super.dispose(); 60 | subscription?.cancel(); 61 | } 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | return MaterialApp( 66 | home: Scaffold( 67 | appBar: AppBar( 68 | title: const Text('flutter_borbidshot example app'), 69 | ), 70 | body: Center( 71 | child: Column( 72 | children: [ 73 | Text('IOS:isCaptured:${isCaptured}'), 74 | RaisedButton( 75 | child: Text('Android forbidshot on'), 76 | onPressed: () { 77 | FlutterForbidshot.setAndroidForbidOn(); 78 | }, 79 | ), 80 | RaisedButton( 81 | child: Text('Android forbidshot off'), 82 | onPressed: () { 83 | FlutterForbidshot.setAndroidForbidOff(); 84 | }, 85 | ), 86 | ], 87 | ), 88 | ), 89 | ), 90 | ); 91 | } 92 | } 93 | ``` 94 | 95 | ## 4.Tip 96 | 97 | 1.使用ios api可以在应用内做监听后暂停视频播放; 98 | 99 | 2.测试android api在小米手机只能拦截截屏,在三星手机可以拦截截屏,录屏后视频内容变成黑色; 100 | 101 | 102 | --- 103 | ##### 完结,撒花🎉 -------------------------------------------------------------------------------- /android/Android实战——系统悬浮窗踩坑记.md: -------------------------------------------------------------------------------- 1 | #### 1.背景介绍 2 | 开启悬浮窗后,小窗悬浮在app内及桌面上,并保持悬浮窗页面所有状态 3 | > 预览 4 | 5 | ![image](http://file.jinxianyun.com/floatwindowdemo.gif) 6 | [video](http://file.jinxianyun.com/floatwindowdemo.mp4) 7 | > 路由介绍 8 | 9 | ![image](http://file.jinxianyun.com/floatwindowroute.jpg) 10 | [image](http://file.jinxianyun.com/floatwindowroute.jpg) 11 | 12 | > 功能概览(⚠️:【】标记处有坑,后面有解释和解决办法) 13 | 1. splash->首页->详情页->悬浮窗页->回到桌面->点击桌面App图标->【悬浮窗页】 14 | 2. splash->首页->详情页->悬浮窗页->开启悬浮窗->点击悬浮窗->悬浮窗页 15 | 2. splash->首页->详情页->悬浮窗页->开启悬浮窗->回到桌面->点击悬浮窗->悬浮窗页->【返回详情页】 16 | 17 | #### 2.功能实现 18 | - 在AndroidManifest.xml将悬浮窗页面android:launchMode="singleInstance" 19 | - 添加权限 20 | 21 | ```xml 22 | 23 | ``` 24 | 25 | - 检查悬浮窗权限 26 | - 点击开启,将悬浮窗页置于后台,同时添加小窗 27 | ```kotlin 28 | moveTaskToBack(true) 29 | 30 | var windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 31 | windowManager.addView(...) 32 | ``` 33 | - 点击小窗将浮窗页置于前台 34 | 35 | ```kotlin 36 | var intent = Intent(it, FloatWindowActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 37 | it.startActivity(intent) 38 | ``` 39 | - 进入悬浮窗页面关闭小窗,保留悬浮窗页面 40 | 41 | ```kotlin 42 | override fun onRestart() { 43 | super.onRestart() 44 | closeFloatWindow(this, exitFloatActivity = false) 45 | } 46 | ``` 47 | 48 | 49 | - 点击关闭小窗, 同时将后台的悬浮窗页面finish 50 | 51 | ```kotlin 52 | closeFloatWindow(this, exitFloatActivity = true) 53 | ``` 54 | 55 | #### 3.踩坑级解决方案 56 | 1. 功能预览中第一条,点击桌面App图标会显示详情页面,期望显示悬浮窗页 57 | > 解释: 58 | 59 | 由于设置了悬浮窗页为singleInstance,所以默认打开的是路由栈A的最上面路由,而不是路由栈B的悬浮窗页面。 60 | > 解决: 61 | 62 | 在悬浮窗页面监听app回到前台,如果不在小窗中,将该路由栈拉回前台 63 | 64 | ```kotlin 65 | if (FloatWindowHelper.instance.dragFloatWrapper == null) { 66 | var intent = Intent(this, FloatWindowActivity::class.java) 67 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 68 | .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) 69 | startActivity(intent) 70 | } 71 | ``` 72 | 73 | 2.功能预览中第三条,悬浮窗页面点击返回会回到桌面,期望回到详情页 74 | > 解释: 75 | 76 | 上一步点击悬浮窗,路由跳转到悬浮窗页面,此时启动的只有路由栈B,所以返回就直接回到桌面了 77 | 78 | 79 | >解决: 80 | 81 | 如果在桌面点击悬浮窗时,先启动app(默认路由栈A),这里有个小细节,会启动我们清单文件里设置了LAUNCHER的页面,即Splash页面,这里只需要finish掉不需要再自动跳到主页 82 | 83 | ``` 84 | // 先启动app,默认standard路由栈 85 | if (isAppInBg) { 86 | context.let { 87 | val intent = it.packageManager.getLaunchIntentForPackage(it.packageName) 88 | it.startActivity(intent) 89 | // 告诉splash不需要跳转到主页,然后再恢复需要跳转 90 | needJumpToMain = false 91 | timer.schedule(object : TimerTask() { 92 | override fun run() {[link](https://note.youdao.com/) 93 | needJumpToMain = true 94 | } 95 | }, 1200) 96 | 97 | } 98 | isAppInBg = false 99 | } 100 | ``` 101 | --- 102 | 源码已上传至[github](https://github.com/qq326646683/FloatWindowDemo) 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ###### [十九、Android学习——Handler通信机制](https://github.com/qq326646683/tech-article/blob/master/android/Android学习——Handler通信机制.md) 2 | 3 | ###### [十八、Android实战——RecyclerView条目曝光埋点](https://github.com/qq326646683/tech-article/blob/master/android/Android实战——RecyclerView条目曝光埋点.md) 4 | 5 | ###### [十七、Android实战——Cocos游戏容器多进程通信](https://github.com/qq326646683/tech-article/blob/master/android/Android实战——Cocos游戏容器多进程通信.md) 6 | 7 | ###### [十六、Android实战——Cocos游戏容器搭建篇](https://github.com/qq326646683/tech-article/blob/master/android/Android实战——Cocos游戏容器搭建篇.md) 8 | 9 | ###### [十五、Android学习——VAP源码](https://github.com/qq326646683/tech-article/blob/master/android/Android学习——VAP源码.md) 10 | 11 | ###### [十四、Flutter上线项目实战——Vap视频动画](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter上线项目实战——Vap视频动画.md) 12 | 13 | ###### [十三、Android JNI实战——记录实现视频播放器](https://github.com/qq326646683/tech-article/blob/master/android/AndroidJNI实战——记录实现视频播放器.md) 14 | 15 | ###### [十二、Android&RN&Flutter实战——防抖节流函数](https://github.com/qq326646683/tech-article/blob/master/common/Android&RN&Flutter实战——防抖节流函数.md) 16 | 17 | ###### [十一、Android实战——系统悬浮窗踩坑记](https://github.com/qq326646683/tech-article/blob/master/android/Android实战——系统悬浮窗踩坑记.md) 18 | 19 | ###### [十、Flutter上线项目实战——性能优化篇(未完待续)](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter上线项目实战——性能优化篇(未完待续).md) 20 | 21 | ###### [九、Flutter上线项目实战——图片视频预览](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter上线项目实战——图片视频预览.md) 22 | 23 | ###### [八、Flutter上线项目实战——环信客服插件](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E7%8E%AF%E4%BF%A1%E5%AE%A2%E6%9C%8D%E6%8F%92%E4%BB%B6.md) 24 | 25 | ###### [七、Flutter上线项目实战——队列任务](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E9%98%9F%E5%88%97%E4%BB%BB%E5%8A%A1.md) 26 | 27 | ###### [六、Flutter上线项目实战——即时通讯Protobuf](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E5%8D%B3%E6%97%B6%E9%80%9A%E8%AE%AFProtobuf.md) 28 | 29 | ###### [五、Flutter上线项目实战——即时通讯端对端加密(E2E)](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E5%8D%B3%E6%97%B6%E9%80%9A%E8%AE%AF%E7%AB%AF%E5%AF%B9%E7%AB%AF%E5%8A%A0%E5%AF%86(E2E).md) 30 | 31 | ###### [四、Flutter上线项目实战——苹果内购](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E8%8B%B9%E6%9E%9C%E5%86%85%E8%B4%AD.md) 32 | 33 | ###### [三、Flutter上线项目实战——腾讯点播直播下载](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E8%85%BE%E8%AE%AF%E7%82%B9%E6%92%AD%E7%9B%B4%E6%92%AD%E4%B8%8B%E8%BD%BD.md) 34 | 35 | ###### [二、Flutter上线项目实战——防止录屏](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E9%98%B2%E6%AD%A2%E5%BD%95%E5%B1%8F.md) 36 | 37 | ###### [一、Flutter上线项目实战——路由篇](https://github.com/qq326646683/tech-article/blob/master/flutter/Flutter%E4%B8%8A%E7%BA%BF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E2%80%94%E2%80%94%E8%B7%AF%E7%94%B1%E7%AF%87.md) -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——即时通讯Protobuf.md: -------------------------------------------------------------------------------- 1 | ## 一、应用背景: 2 | [Protobuf](https://github.com/protocolbuffers/protobuf)是google 的一种数据交换的格式,它独立于语言,独立于平台。 3 | 4 | 5 | 优点: 6 | 7 | - json优点就是较XML格式更加小巧,传输效率较xml提高了很多,可读性还不错。 8 | - xml优点就是可读性强,解析方便。 9 | - protobuf优点就是传输效率快(据说在数据量大的时候,传输效率比xml和json快10-20倍),序列化后体积相比Json和XML很小,支持跨平台多语言,消息格式升级和兼容性还不错,序列化反序列化速度很快。 10 | 11 | 缺点: 12 | 13 | - json缺点就是传输效率也不是特别高(比xml快,但比protobuf要慢很多)。 14 | - xml缺点就是效率不高,资源消耗过大。 15 | - protobuf缺点就是使用不太方便。 16 | 17 | 在一个需要大量的数据传输的场景中,如果数据量很大,那么选择protobuf可以明显的减少数据量,减少网络IO,从而减少网络传输所消耗的时间。考虑到作为一个主打社交的产品,消息数据量会非常大,同时为了节约流量,所以采用protobuf是一个不错的选择。 18 | 19 | 20 | 21 | ## 二、使用 22 | 23 | ### 1.引入protobuf库 24 | 25 | > pubspec.yaml 26 | 27 | ```dart 28 | ... 29 | 30 | protobuf: 1.0.1 31 | 32 | ``` 33 | 34 | ### 2.编写proto文件 35 | > socket.message.proto 36 | 37 | ```dart 38 | syntax = "proto3"; 39 | package socket; 40 | 41 | // 发送聊天信息 42 | message Message { 43 | string eventId = 1; 44 | string from = 2; 45 | string to = 3; 46 | string createAt = 4; 47 | string type = 5; 48 | string body = 6; 49 | } 50 | 51 | // 收到聊天消息 52 | message AckMessage { 53 | string eventId = 1; 54 | } 55 | ``` 56 | ### 3.生成proto相关Model 57 | > Terminal 58 | ``` 59 | protoc --dart_out=. socket.message.proto 60 | ``` 61 | 62 | ### 4.编码、发消息 63 | > a.准备protobuf对象 64 | 65 | ```dart 66 | Message message = Message(); 67 | message.eventId = '####'; 68 | message.type = 'text'; 69 | message.body = 'hello world'; 70 | ``` 71 | 72 | > b.ProtobufUtil编码 73 | 74 | ```dart 75 | const MESSAGE_HEADER_LEN = 2; 76 | /// 数据编码 77 | static List encode(int type, var content) { 78 | ByteData data = ByteData(MESSAGE_HEADER_LEN); 79 | data.setUint16(0, type, Endian.little); 80 | List msg = data.buffer.asUint8List() + content.writeToBuffer().buffer.asUint8List(); 81 | return msg; 82 | } 83 | 84 | ``` 85 | > c.发消息 86 | 87 | ```dart 88 | /// 发送 89 | sendSocket(int type, var content) async { 90 | IOWebSocketChannel channel = await SocketService.getInstance().getChannel(); 91 | if (channel == null) return; 92 | List msg = ProtobufUtil.encode(type, content); 93 | channel.sink.add(msg); 94 | } 95 | 96 | sendSocket(11, message) 97 | ``` 98 | 99 | 100 | 101 | ### 5.收消息、解码 102 | 103 | > a.解码 104 | 105 | ```dart 106 | /// 数据解码 107 | static DecodedMsg decode(data) { 108 | Int8List int8Data = Int8List.fromList(data); 109 | Int8List contentTypeInt8Data = int8Data.sublist(0, MESSAGE_HEADER_LEN); 110 | Int8List contentInt8Data = int8Data.sublist(MESSAGE_HEADER_LEN, int8Data.length); 111 | int contentType = contentTypeInt8Data.elementAt(0); 112 | 113 | 114 | GeneratedMessage content; 115 | switch (contentType) { 116 | case 10: 117 | content = AckMessage.fromBuffer(contentInt8Data); 118 | break; 119 | case 11: 120 | content = Message.fromBuffer(contentInt8Data); 121 | break; 122 | } 123 | 124 | DecodedMsg decodedMsg; 125 | if (contentType != null && content != null) { 126 | decodedMsg = DecodedMsg( 127 | contentType: contentType, 128 | content: content, 129 | ); 130 | } 131 | return decodedMsg; 132 | } 133 | 134 | 135 | ``` 136 | 137 | > b.收消息 138 | 139 | ```dart 140 | channel.stream.listen((data) { 141 | DecodedMsg msg = ProtobufUtil.decode(data); 142 | } 143 | 144 | ``` 145 | 146 | --- 147 | 完结,撒花🎉 -------------------------------------------------------------------------------- /cc++/c学习——基础.md: -------------------------------------------------------------------------------- 1 | ###### 一、指针初识 2 | 3 | ```c 4 | int i = 2; 5 | int *ptr_i = &i; 6 | 7 | //i的地址:0x7ffee438a468 8 | printf("%p\n", &i); 9 | //ptr_i为i的地址:0x7ffee438a468 10 | printf("%p\n", ptr_i); 11 | //指针自己的地址:0x7ffee4d98460 12 | printf("%p\n", &ptr_i); 13 | //该指针指向的地址对应的值:2 14 | printf("%d\n", *ptr_i); 15 | 16 | //将指针指向的地址赋值给另外一个指针:0x7ffeee6e7468 17 | //void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针 18 | void *ptr2 = ptr_i; 19 | printf("%p\n", ptr2); 20 | ``` 21 | 22 | 23 | 24 | ###### 二、内存管理 25 | 26 | 1. **malloc(int num); ** //在堆区分配一块指定大小的内存空间,用来存放数据 27 | 28 | 2. **realloc(void \*address, int newsize);**//该函数重新分配内存,把内存扩展到newsize 29 | 30 | 3. **calloc(int num, int size)**;//在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0 31 | 32 | 4. **free(void \*address);**//释放 address 所指向的内存块,释放的是动态分配的内存空间 33 | 34 | 35 | 36 | ###### 三、值传递和指针传递 37 | 38 | ```c 39 | void swapValue(int x, int y) { 40 | int temp = x; 41 | x = y; 42 | y = temp; 43 | } 44 | //交换的是指针指向地址存的值,并非交换地址 45 | void swapPointer(int* x, int* y) { 46 | int temp = *x; 47 | *x = *y; 48 | *y = temp; 49 | } 50 | 51 | int main() { 52 | printf("Hello, World2!\n"); 53 | int i = 1; 54 | int j = 2; 55 | swapValue(i, j); 56 | //i: 1, j: 2 57 | printf("i: %d, j: %d\n", i, j); 58 | 59 | //交换前地址:i: 0x7ffee012d46c, j: 0x7ffee012d468 60 | printf("交换前地址:i: %p, j: %p\n", &i, &j); 61 | swapPointer(&i, &j); 62 | //交换后地址:i: 0x7ffee012d46c, j: 0x7ffee012d468 63 | printf("交换后地址:i: %p, j: %p\n", &i, &j); 64 | //i: 2, j: 1 65 | printf("i: %d, j: %d", i, j); 66 | } 67 | 68 | ``` 69 | 70 | 71 | 72 | ###### 四、指针向上、向下转型 73 | 74 | ```c 75 | int n; 76 | int *ptr_n = &n; 77 | 78 | //向上转型 79 | void *ptr_void = ptr_n; 80 | //向下转型 81 | int *ptr_n_new = (int *) ptr_void; 82 | 83 | //ptr_n: 0x7ffee8e8b47c 84 | //ptr_void: 0x7ffee8e8b47c 85 | //ptr_n_new: 0x7ffee8e8b47c 86 | printf("ptr_n: %p\n", ptr_n); 87 | printf("ptr_void: %p\n", ptr_void); 88 | printf("ptr_n_new: %p\n", ptr_n_new); 89 | ``` 90 | 91 | 92 | 93 | ###### 五、指针的加减法 94 | 95 | ```c 96 | int n; 97 | int *ptr_n = &n; 98 | //ptr_n: 0x7ffee8f0646c 99 | //ptr_n+1: 0x7ffee8f06470 100 | printf("ptr_n: %p\n", ptr_n); 101 | printf("ptr_n+1: %p\n", ptr_n + 1); 102 | 103 | short s; 104 | short *ptr_s = &s; 105 | //ptr_s: 0x7ffee8f0645e 106 | //ptr_s+1: 0x7ffee8f06460 107 | printf("ptr_s: %p\n", ptr_s); 108 | printf("ptr_s+1: %p\n", ptr_s + 1); 109 | ``` 110 | 111 | 112 | 113 | ###### 六、数组指针及加减法 114 | 115 | ```c 116 | int array[] = {1, 2, 3, 4, 5}; 117 | //数组array即:数组的第一个元素地址,等价于&array等价于&array[0] 118 | printf("array: %p\n", array); //0x7ffeeb75c450 119 | printf("array + 1: %p\n", array + 1); //0x7ffeeb75c454 120 | printf("&array: %p\n", &array); //0x7ffeeb75c450 121 | printf("&array[0]: %p\n", &array[0]); //0x7ffeeb75c450 122 | 123 | //取数组第二个元素 124 | printf("array[2]: %d\n", array[2]); //3 125 | printf("*(array + 2): %d\n", *(array + 2)); //3 126 | printf("*&array[2]: %d\n", *&array[2]); //3 127 | ``` 128 | 129 | 130 | 131 | ###### 七、函数指针 132 | 133 | ```c 134 | void test(int x, int y) { 135 | printf("printX: %d, printY: %d", x, y); 136 | } 137 | int main() { 138 | //声明函数指针 139 | void (*p)(int, int); 140 | p = test; //等价于p = &test 141 | p(666, 777); 142 | } 143 | ``` 144 | 145 | > 函数作为参数 146 | 147 | ```c 148 | int doOperation(int x, int y, int (*invoke)(int, int)) { 149 | return invoke(x, y); 150 | } 151 | int plus(int x, int y) { 152 | return x + y; 153 | } 154 | int main() { 155 | //doOperation result: 6 156 | printf("doOperation result: %d", doOperation(1 ,5, plus)); 157 | } 158 | ``` 159 | 160 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——队列任务.md: -------------------------------------------------------------------------------- 1 | ## 一、应用场景 2 | 3 | - 队列压缩视频 4 | - 队列解密视频 5 | - 队列请求网络 6 | - 等等 7 | 8 | ## 二、实现思路 9 | 10 | 1. 定义一个任务队列taskList [先进先出] 11 | 2. 提供添加任务方法 12 | 3. 取第一个任务执行 13 | 4. 执行完后,从taskList移除 14 | 5. 递归获取第一个任务并执行任务 15 | 6. 直到taskList为空停止队列任务 16 | 17 | ## 三、具体实现 18 | >支持空安全版本 [戳我](https://github.com/qq326646683/flutter_vap/blob/main/lib/queue_util.dart) 19 | 20 | > QueueUtil.dart 21 | 22 | ```dart 23 | class QueueUtil { 24 | /// 用map key存储多个QueueUtil单例,目的是隔离多个类型队列任务互不干扰 25 | /// Use map key to store multiple QueueUtil singletons, the purpose is to isolate multiple types of queue tasks without interfering with each other 26 | static Map _instance = Map(); 27 | 28 | static QueueUtil get(String key) { 29 | if (_instance[key] == null) { 30 | _instance[key] = QueueUtil._(); 31 | } 32 | return _instance[key]; 33 | } 34 | 35 | QueueUtil._() { 36 | /// 初始化代码 37 | } 38 | 39 | List<_TaskInfo> _taskList = []; 40 | bool _isTaskRunning = false; 41 | int _mId = 0; 42 | bool _isCancelQueue = false; 43 | 44 | Future<_TaskInfo> addTask(Function doSomething) { 45 | _isCancelQueue = false; 46 | _mId++; 47 | _TaskInfo taskInfo = _TaskInfo(_mId, doSomething); 48 | 49 | /// 创建future 50 | Completer<_TaskInfo> taskCompleter = Completer<_TaskInfo>(); 51 | 52 | /// 创建当前任务stream 53 | StreamController<_TaskInfo> streamController = new StreamController(); 54 | taskInfo.controller = streamController; 55 | 56 | /// 添加到任务队列 57 | _taskList.add(taskInfo); 58 | 59 | /// 当前任务的stream添加监听 60 | streamController.stream.listen((_TaskInfo completeTaskInfo) { 61 | if (completeTaskInfo.id == taskInfo.id) { 62 | taskCompleter.complete(completeTaskInfo); 63 | streamController.close(); 64 | } 65 | }); 66 | 67 | /// 触发任务 68 | _doTask(); 69 | 70 | return taskCompleter.future; 71 | } 72 | 73 | void cancelTask() { 74 | _taskList = []; 75 | _isCancelQueue = true; 76 | _mId = 0; 77 | _isTaskRunning = false; 78 | } 79 | 80 | _doTask() async { 81 | if (_isTaskRunning) return; 82 | if (_taskList.isEmpty) return; 83 | 84 | /// 取任务 85 | _TaskInfo taskInfo = _taskList[0]; 86 | _isTaskRunning = true; 87 | 88 | /// 模拟执行任务 89 | await taskInfo.doSomething?.call(); 90 | 91 | taskInfo.controller.sink.add(taskInfo); 92 | 93 | if (_isCancelQueue) return; 94 | 95 | /// 出队列 96 | _taskList.removeAt(0); 97 | _isTaskRunning = false; 98 | 99 | /// 递归执行任务 100 | _doTask(); 101 | } 102 | } 103 | 104 | class _TaskInfo { 105 | int id; // 任务唯一标识 106 | Function doSomething; 107 | StreamController<_TaskInfo> controller; 108 | 109 | _TaskInfo(this.id, this.doSomething, {this.controller}); 110 | } 111 | ``` 112 | 113 | ## 四、使用 114 | 115 | ```dart 116 | main() { 117 | /// 将任务添加到队列 118 | print("加入队列-net, taskNo: 1"); 119 | QueueUtil.get("net").addTask(() { 120 | return _doFuture("net", 1); 121 | }); 122 | print("加入队列-net, taskNo: 2"); 123 | QueueUtil.get("net").addTask(() { 124 | return _doFuture("net", 2); 125 | }); 126 | print("加入队列-local, taskNo: 1"); 127 | QueueUtil.get("local").addTask(() { 128 | return _doFuture("local", 1); 129 | }); 130 | 131 | 132 | 133 | /// 取消队列任务 134 | /// QueueUtil.get("net").cancelTask(); 135 | } 136 | 137 | 138 | 139 | Future _doFuture(String queueName, int taskNo) { 140 | return Future.delayed(Duration(seconds: 2), () { 141 | print("任务完成 queueName: $queueName, taskNo: $taskNo"); 142 | }); 143 | } 144 | 145 | 146 | // 执行结果: 147 | I/flutter (26436): 加入队列-net, taskNo: 1 148 | I/flutter (26436): 加入队列-net, taskNo: 2 149 | I/flutter (26436): 加入队列-local, taskNo: 1 150 | ------------两秒后-------- 151 | I/flutter (26436): 任务完成 queueName: net, taskNo: 1 152 | I/flutter (26436): 任务完成 queueName: local, taskNo: 1 153 | ------------两秒后-------- 154 | I/flutter (26436): 任务完成 queueName: net, taskNo: 2 155 | ``` 156 | 157 | 158 | --- 159 | 完结,撒花🎉 160 | 161 | 162 | -------------------------------------------------------------------------------- /common/Android&RN&Flutter实战——防抖节流函数.md: -------------------------------------------------------------------------------- 1 | > ## 1.背景介绍 2 | 3 | ### 防抖 4 | 函数防抖,这里的抖动就是执行的意思,而一般的抖动都是持续的,多次的。假设函数持续多次执行,我们希望让它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。 5 | 6 | 7 | ### 节流 8 | 节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。 9 |

10 | > ## 2.经典举例 11 | 12 | 1. 防抖函数:搜索页面,用户连续输入,等停下来再去触发搜索接口 13 | 2. 节流函数:防止按钮连点 14 | 15 |
16 | 17 | > ## 3.Android实现 18 | 19 | 1. 代码实现: 20 | 21 | ```kotlin 22 | object FunctionUtil { 23 | private const val DEFAULT_DURATION_TIME = 300L 24 | var timer: Timer? = null 25 | 26 | 27 | /** 28 | * 防抖函数 29 | */ 30 | fun debounce(duration: Long = DEFAULT_DURATION_TIME, doThing: () -> Unit) { 31 | timer?.cancel() 32 | timer = Timer().apply { 33 | schedule(timerTask { 34 | doThing.invoke() 35 | timer = null 36 | }, duration) 37 | } 38 | } 39 | 40 | /** 41 | * 节流函数 42 | */ 43 | var lastTimeMill = 0L 44 | fun throttle(duration: Long = DEFAULT_DURATION_TIME, continueCall: (() -> Unit)? = null, doThing: () -> Unit) { 45 | val currentTime = System.currentTimeMillis() 46 | if (currentTime - lastTimeMill > duration) { 47 | doThing.invoke() 48 | lastTimeMill = System.currentTimeMillis() 49 | } else { 50 | continueCall?.invoke() 51 | } 52 | } 53 | } 54 | ``` 55 | 2. 使用: 56 | ```kotlin 57 | btn_sure.setOnClickListener { 58 | FunctionUtil.throttle { 59 | Log.i("nell-click", "hahah") 60 | } 61 | } 62 | 63 | btn_sure.setOnClickListener { 64 | FunctionUtil.throttle(500L) { 65 | Log.i("nell-click", "hahah") 66 | } 67 | } 68 | 69 | FunctionUtil.debounce { 70 | searchApi(text) 71 | } 72 | ``` 73 | 74 | > ## 4.RN实现 75 | 76 | 1. 代码实现: 77 | ```javascript 78 | /** 79 | * 防抖函数 80 | */ 81 | function debounce(func, delay) { 82 | let timeout 83 | return function() { 84 | clearTimeout(timeout) 85 | timeout = setTimeout(() => { 86 | func.apply(this, arguments) 87 | }, delay) 88 | } 89 | } 90 | 91 | /** 92 | * 节流函数 93 | */ 94 | function throttle(func, delay) { 95 | let run = true 96 | return function () { 97 | if (!run) { 98 | return 99 | } 100 | run = false // 持续触发的话,run一直是false,就会停在上边的判断那里 101 | setTimeout(() => { 102 | func.apply(this, arguments) 103 | run = true // 定时器到时间之后,会把开关打开,我们的函数就会被执行 104 | }, delay) 105 | } 106 | } 107 | 108 | ``` 109 | 110 | 2. 使用: 111 | 112 | ```javascript 113 | throttle(function (e) { 114 | console.log("nell-click") 115 | }, 1000) 116 | 117 | 118 | debounce(function (e) { 119 | searchApi(text) 120 | }, 300) 121 | ``` 122 | 123 | 124 | > ## 5.Flutter实现 125 | 126 | 1. 代码实现: 127 | ```dart 128 | class CommonUtil { 129 | static const deFaultDurationTime = 300; 130 | static Timer timer; 131 | 132 | // 防抖函数 133 | static debounce(Function doSomething, {durationTime = deFaultDurationTime}) { 134 | timer?.cancel(); 135 | timer = new Timer(Duration(milliseconds: durationTime), () { 136 | doSomething?.call(); 137 | timer = null; 138 | }); 139 | } 140 | 141 | // 节流函数 142 | static const String deFaultThrottleId = 'DeFaultThrottleId'; 143 | static Map startTimeMap = {deFaultThrottleId: 0}; 144 | static throttle(Function doSomething, {String throttleId = deFaultThrottleId, durationTime = deFaultDurationTime, Function continueClick}) { 145 | int currentTime = DateTime.now().millisecondsSinceEpoch; 146 | if (currentTime - (startTimeMap[throttleId] ?? 0) > durationTime) { 147 | doSomething?.call(); 148 | startTimeMap[throttleId] = DateTime.now().millisecondsSinceEpoch; 149 | } else { 150 | continueClick?.call(); 151 | } 152 | } 153 | 154 | } 155 | ``` 156 | 2. 使用: 157 | 158 | ```dart 159 | GestureDetector( 160 | onTap: () => CommonUtil.throttle(onTap, durationTime: durationTime) 161 | ) 162 | 163 | 164 | CommonUtil.debounce(searchApi) 165 | ``` 166 | 167 | 168 | --- 169 | 完结,撒花🎉 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /android/Android实战——Cocos游戏容器搭建篇.md: -------------------------------------------------------------------------------- 1 | ### 一、前言 2 | 现在市面上很多app有游戏中心功能,最早的有微信小游戏和QQ小游戏,再后来像bilibili、喜马拉雅、爱奇艺、比心等等应用中也加入了游戏中心模块。本篇文章将介绍如何上手搭建cocos creater游戏容器,先来看看效果: 3 | 4 | ![image](http://file.jinxianyun.com/cocos_android.gif) 5 | 6 | ### 二、准备工作 7 | 1. [下载](https://www.cocos.com/creator)并安装最新版本CocosDashboard 8 | 2. 在Dashborad下载最新版本编辑器 9 | 10 | ![image](http://file.jinxianyun.com/cocos_creator_install.jpg) 11 | 12 | 3. 在Android Studio安装NDK,我这里安装的是21.1.6352462,目前为止比较稳定 13 | 14 | ![image](http://file.jinxianyun.com/ndk_install.png) 15 | 16 | 4. 在CocosDashboard新建HelloWorld项目并打开运行,我这里用的3.1.1版本 17 | 18 | ![image](http://file.jinxianyun.com/cocos_hellerun.png) 19 | 20 | 5. 打开CocosCreator菜单栏偏好设置,在外部程序栏中设置Android NDK和Android SDK路径 21 | 22 | ![image](http://file.jinxianyun.com/cococreator_setup.jpg) 23 | 24 | ### 三、构建cocos游戏.so文件 25 | 1. 在CocosCreator菜单栏选择项目-构建发布,选择发布平台:安卓,点击构建,等大概几分钟 26 | 27 | ![image](http://file.jinxianyun.com/cocos_build.png) 28 | 29 | 2. 成功后,用Android Studio打开文件夹里生成的proj项目,并运行该项目到手机上,这里游戏资源加载的是proj同级目录assets,后续,我们会将assets压缩包zip存放在我们服务器,达到用户下载解压后加载启动游戏的目的。 30 | 31 | 3. 为了后续游戏容器能加载本地filePath下的游戏资源,需要修改JniCocosActivity.cpp里的Java_com_cocos_lib_CocosActivity_onCreateNative方法 32 | 33 | ![image](http://file.jinxianyun.com/cocos_modify.png) 34 | 35 | 36 | 4. ./gradlew assembleRelease打release包, 将instantapp-release.apk后缀改成zip,解压后获取lib下arm64-v8a/armeabi-v7a下的libcocos.so(构建版本设置那里可以勾选不同架构) 37 | 38 | 39 | ### 四、制作自己的游戏容器 40 | 1. 创建module,包名为com.cocos.lib(为了和.so文件里保持一致,不然无法调用c方法) 41 | 42 | 2. module的清单文件加 43 | ``` 44 | 45 | ``` 46 | 47 | 3. 将/Applications/CocosCreator/Creator/3.1.1/CocosCreator.app/Contents/Resources/resources/3d/engine-native/cocos/platform/android/java/libs拷贝到module/libs下 48 | 49 | 4. module下build.gradle添加 50 | ``` 51 | implementation fileTree(include: ['*.jar'], dir: 'libs') 52 | ``` 53 | 54 | 5. 将.so文件放在module/src/main/jniLibs/下 55 | 6. 将/Applications/CocosCreator/Creator/3.1.1/CocosCreator.app/Contents/Resources/resources/3d/engine-native/cocos/platform/android/java/src/com/cocos/lib下的java文件复制到module/src/main/java/com.cocos.lib下 56 | 7. 修改文件CocosActivity.java,因为游戏页面官方推荐用多进程来做,所以这里退出游戏,即将游戏进程kill 57 | ```java 58 | // 加一个filePath参数 59 | private native void onCreateNative(Activity activity, AssetManager assetManager, String obbPath, int sdkVersion, String filePath); 60 | 61 | // 外部传入游戏资源路径 62 | protected String filePath() { 63 | return ""; 64 | } 65 | 66 | @Override 67 | protected void onCreate(Bundle savedInstanceState) { 68 | ... 69 | onCreateNative(this, getAssets(), getAbsolutePath(getObbDir()), Build.VERSION.SDK_INT, filePath()); 70 | } 71 | 72 | @Override 73 | public void onBackPressed() { 74 | super.onBackPressed(); 75 | System.exit(0); 76 | } 77 | ``` 78 | ### 五、总结 79 | 自此,我们游戏容器制作完毕,我也将该篇的游戏容器module传到了jitpack,可以直接使用: 80 | ``` 81 | allprojects { 82 | repositories { 83 | ... 84 | maven { url 'https://jitpack.io' } 85 | } 86 | } 87 | ``` 88 | ``` 89 | dependencies { 90 | implementation 'com.github.qq326646683:cocos-creator-android:1.0.0' 91 | } 92 | ``` 93 | 94 | ### 六、如何使用 95 | 1. 文件读写、网络权限 96 | ```xml 97 | 98 | 99 | 100 | ``` 101 | 2. 下载游戏zip并解压 102 | 3. 继承CocosActivity,并将解压后的路径赋值给filePath 103 | ```kotlin 104 | class CocosGameActivity: CocosActivity() { 105 | override fun onCreate(savedInstanceState: Bundle?) { 106 | super.onCreate(savedInstanceState) 107 | } 108 | 109 | override fun filePath() = intent.getStringExtra("path") 110 | } 111 | ``` 112 | 4. 清单文件 113 | ``` 114 | 115 | 118 | 119 | ``` 120 | 121 | 5. 本篇的module和事例app代码放在[gitlab](https://github.com/qq326646683/cocos-creator-android) 122 | 123 | ### 七、后续计划 124 | cocos游戏和android通信,因为牵扯到多进程,通信变的麻烦,后续计划将这部分内容封装在module library,方便使用者调用 125 | 126 | 127 | --- 128 | 完结,撒花🎉 129 | -------------------------------------------------------------------------------- /android/Android实战——RecyclerView条目曝光埋点.md: -------------------------------------------------------------------------------- 1 | ### 一、概要 2 | 100行代码实现recyclerview条目曝光埋点设计 3 | 4 | ### 二、设计思路 5 | 1. 条目露出来一半以上视为该条目曝光。 6 | 2. 在rv滚动过程中或者数据变更回调OnGlobalLayoutListener时,将符合条件1的条目记录在曝光列表、上传埋点集合里。 7 | 3. 滚动状态变更和OnGlobalLayoutListener回调时,且列表状态为idle状态,触发上报埋点。 8 | 9 | ### 三、容错性 10 | 1. 滑动过快时,视为未曝光 11 | 2. 数据变更时,重新检测曝光 12 | 3. 曝光过的条目,不会重复曝光 13 | 14 | ### 四、接入影响 15 | 1. 对业务代码零侵入 16 | 2. 对列表滑动体验无影响 17 | 18 | ### 五、代码实现 19 | ```kotlin 20 | import android.graphics.Rect 21 | import android.view.View 22 | import androidx.recyclerview.widget.RecyclerView 23 | import java.util.* 24 | 25 | class RVItemExposureListener( 26 | private val mRecyclerView: RecyclerView, 27 | private val mExposureListener: IOnExposureListener? 28 | ) { 29 | interface IOnExposureListener { 30 | fun onExposure(position: Int) 31 | fun onUpload(exposureList: List?): Boolean 32 | } 33 | 34 | private val mExposureList: MutableList = ArrayList() 35 | private val mUploadList: MutableList = ArrayList() 36 | private var mScrollState = 0 37 | 38 | var isEnableExposure = true 39 | private var mCheckChildViewExposure = true 40 | 41 | private val mViewVisible = Rect() 42 | fun checkChildExposeStatus() { 43 | if (!isEnableExposure) { 44 | return 45 | } 46 | val length = mRecyclerView.childCount 47 | if (length != 0) { 48 | var view: View? 49 | for (i in 0 until length) { 50 | view = mRecyclerView.getChildAt(i) 51 | if (view != null) { 52 | view.getLocalVisibleRect(mViewVisible) 53 | if (mViewVisible.height() > view.height / 2 && mViewVisible.top < mRecyclerView.bottom) { 54 | checkExposure(view) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | private fun checkExposure(childView: View): Boolean { 62 | val position = mRecyclerView.getChildAdapterPosition(childView) 63 | if (position < 0 || mExposureList.contains(position)) { 64 | return false 65 | } 66 | mExposureList.add(position) 67 | mUploadList.add(position) 68 | mExposureListener?.onExposure(position) 69 | return true 70 | } 71 | 72 | private fun uploadList() { 73 | if (mScrollState == RecyclerView.SCROLL_STATE_IDLE && mUploadList.size > 0 && mExposureListener != null) { 74 | val success = mExposureListener.onUpload(mUploadList) 75 | if (success) { 76 | mUploadList.clear() 77 | } 78 | } 79 | } 80 | 81 | init { 82 | mRecyclerView.viewTreeObserver.addOnGlobalLayoutListener { 83 | if (mRecyclerView.childCount == 0 || !mCheckChildViewExposure) { 84 | return@addOnGlobalLayoutListener 85 | } 86 | checkChildExposeStatus() 87 | uploadList() 88 | mCheckChildViewExposure = false 89 | } 90 | mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { 91 | override fun onScrollStateChanged( 92 | recyclerView: RecyclerView, 93 | newState: Int 94 | ) { 95 | super.onScrollStateChanged(recyclerView, newState) 96 | mScrollState = newState 97 | uploadList() 98 | } 99 | 100 | override fun onScrolled( 101 | recyclerView: RecyclerView, 102 | dx: Int, 103 | dy: Int 104 | ) { 105 | super.onScrolled(recyclerView, dx, dy) 106 | if (!isEnableExposure) { 107 | return 108 | } 109 | 110 | // 大于50视为滑动过快 111 | if (mScrollState == RecyclerView.SCROLL_STATE_SETTLING && Math.abs(dy) > 50) { 112 | return 113 | } 114 | checkChildExposeStatus() 115 | } 116 | }) 117 | } 118 | } 119 | 120 | ``` 121 | 122 | ### 六、使用 123 | ```kotlin 124 | RVItemExposureListener(yourRecyclerView, object : RVItemExposureListener.IOnExposureListener { 125 | override fun onExposure(position: Int) { 126 | // 滑动过程中出现的条目 127 | Log.d("exposure-curPosition:", position.toString()) 128 | } 129 | 130 | override fun onUpload(exposureList: List?): Boolean { 131 | Log.d("exposure-positionList", exposureList.toString()) 132 | // 上报成功后返回true 133 | return true 134 | } 135 | 136 | }) 137 | ``` 138 | 139 | --- 140 | 完结,撒花🎉 141 | 142 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——性能优化篇(未完待续).md: -------------------------------------------------------------------------------- 1 | # 例子 1、 2 | #### 现有一个滑动聊天页面出现/隐藏jump按钮的需求: 3 | 4 | ![image](http://file.jinxianyun.com/flutter_optimize1.gif) 5 | [🔗](http://file.jinxianyun.com/flutter_optimize1.gif) 6 | 7 | 8 | > 1. 优化前:ListView外层setState实现: 9 | 10 | ```dart 11 | class _ChatListViewWidgetState extends State { 12 | ScrollController scrollController; 13 | double _chatListOffset = 0; 14 | 15 | @override 16 | void initState() { 17 | super.initState(); 18 | 19 | scrollController = new ScrollController() 20 | ..addListener(() { 21 | setState(() { 22 | _chatListOffset = scrollController.offset; 23 | }); 24 | }); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Stack( 30 | alignment: Alignment.bottomCenter, 31 | children: [ 32 | ListView.builder( 33 | controller: _scrollController, 34 | ... 35 | ), 36 | _chatListOffset > 150 ? Image.asset('jump.png') : SizedBox() 37 | ], 38 | ); 39 | } 40 | } 41 | 42 | ``` 43 | 44 | > 2. 优化后:在JumpButton内部setState实现 45 | 46 | ``` dart 47 | class JumpButton extends StatefulWidget { 48 | ScrollController scrollController; 49 | 50 | JumpButton({this.scrollController}); 51 | 52 | @override 53 | _JumpButtonState createState() => _JumpButtonState(); 54 | } 55 | 56 | class _JumpButtonState extends State { 57 | double _chatListOffset = 0; 58 | 59 | @override 60 | void initState() { 61 | super.initState(); 62 | widget.scrollController.addListener(() { 63 | setState(() { 64 | _chatListOffset = widget.scrollController.offset; 65 | }); 66 | }); 67 | } 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | return _chatListOffset > 150 ? Image.asset('jump.png') : SizedBox(); 72 | } 73 | } 74 | 75 | ``` 76 | > 3. 开启 DevTools 的 Repaint RainBow差别: 77 | 78 | ![image](http://file.jinxianyun.com/flutter_optimize2.gif) 79 | [🔗](http://file.jinxianyun.com/flutter_optimize2.gif) 80 | 81 | ###### 左边是优化前: 发现在滑动过程中,ListView在不断的绘制,右边为优化后,在按钮出现的时候绘制一次 82 | 83 | > 4. 思考与总结: 84 | ###### setState会导致当前页面根节点开始遍历,所以我们要在需要重新渲染的Widget内部进行setState更新自己,可以有效防止多余的树节点遍历。参考flutter官方的性能测试视频也有相应的讲解:https://www.bilibili.com/video/BV1F4411D7rP 的13’开始 85 | 86 | 87 | 88 | # 例子 2、 89 | #### 现有一个在聊天页面上层悬浮一个跑马灯的需求: 90 | 91 | > 1. 功能实现: 92 | 93 | ``` dart 94 | // 聊天页面 95 | Stack( 96 | alignment: Alignment.bottomCenter, 97 | children: [ 98 | ListView.builder(...), 99 | /// 跑马灯 100 | MMarqueeWidget(), 101 | ], 102 | ) 103 | ``` 104 | 105 | ###### 参考徐医生的[dojo](https://github.com/xuyisheng/flutter_dojo/blob/a0020faa635022ba8a2c47b41bf0668a27e2d1f3/lib/category/pattern/texteffect/marquee.dart) 106 | ``` dart 107 | // 跑马灯组件实现, 108 | import 'package:flutter/material.dart'; 109 | 110 | class MMarqueeWidget extends StatefulWidget { 111 | final Widget child; 112 | 113 | MMarqueeWidget({Key key, this.child}) : super(key: key); 114 | 115 | @override 116 | _MMarqueeWidgetState createState() => _MMarqueeWidgetState(); 117 | } 118 | 119 | class _MMarqueeWidgetState extends State with SingleTickerProviderStateMixin { 120 | AnimationController controller; 121 | Animation animation; 122 | 123 | @override 124 | void initState() { 125 | super.initState(); 126 | controller = AnimationController(vsync: this, duration: Duration(seconds: 10)); 127 | animation = Tween(begin: Offset(1, 0), end: Offset(-1, 0)).animate(controller); 128 | controller.repeat(); 129 | } 130 | 131 | @override 132 | Widget build(BuildContext context) { 133 | return AnimatedBuilder( 134 | animation: animation, 135 | builder: (context, _) { 136 | return ClipRect( 137 | child: FractionalTranslation( 138 | translation: animation.value, 139 | child: SingleChildScrollView( 140 | scrollDirection: Axis.horizontal, 141 | child: widget.child, 142 | ), 143 | ), 144 | ); 145 | }, 146 | ); 147 | } 148 | 149 | @override 150 | void dispose() { 151 | controller.dispose(); 152 | super.dispose(); 153 | } 154 | } 155 | ``` 156 | 157 | > 2. 开启 DevTools 的 Repaint RainBow 158 | 159 | ![image](http://file.jinxianyun.com/flutter_optimize3.gif) 160 | [🔗](http://file.jinxianyun.com/flutter_optimize3.gif) 161 | 162 | ###### 发现跑马灯动画会导致其他部分也重绘 163 | 164 | > 3. RepaintBoundary优化 165 | 166 | ```dart 167 | @override 168 | Widget build(BuildContext context) { 169 | return RepaintBoundary( 170 | child: AnimatedBuilder( 171 | animation: animation, 172 | ... 173 | ), 174 | ); 175 | } 176 | ``` 177 | 178 | ###### flutter提供了创建一个单独的图层,不会影响其他图层,参考https://www.bilibili.com/video/BV1F4411D7rP的24’20’’或者唯鹿的这篇https://www.jianshu.com/p/99d8a42f6704 179 | 180 | 181 | 182 | --- 183 | ❤️未完待续... -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——即时通讯端对端加密(E2E).md: -------------------------------------------------------------------------------- 1 | > ### 1.背景知识 2 | 3 | - 数字签名算法EdDSA: ed25519 4 | - 密钥交换算法ECDH: x25519 5 | - 加密算法AES 6 | - 参考阅读: http://www.freebuf.com/articles/database/113855.html 7 | 8 | > ### 2.设计思路 9 | 10 | ![image](http://file.jinxianyun.com/chate2e.jpg) 11 | 12 | ###### [>> image link](http://file.jinxianyun.com/chate2e.jpg) 13 | 14 | > ### 3.流程代码 15 | 16 | 1. 引入加解密库 17 | 18 | ```yaml 19 | cryptography: ^1.4.0 20 | ``` 21 | 22 | 2. 主流程 23 | ```dart 24 | static Future mainLogic() async { 25 | // User A 生成密钥 26 | var keyPairEd25519App1 = await ed25519.newKeyPair(); 27 | var keyPairX25519App1 = await x25519.newKeyPair(); 28 | // User B 生成密钥 29 | var keyPairEd25519App2 = await ed25519.newKeyPair(); 30 | var keyPairX25519App2 = await x25519.newKeyPair(); 31 | 32 | // User A通过 ed25519 密钥对对 x25519 的公钥进行签名 33 | var signature1 = await EncryptUtil.sign(keyPairX25519App1.publicKey.bytes, keyPairEd25519App1); 34 | // User B通过 ed25519 密钥对对 x25519 的公钥进行签名 35 | var signature2 = await EncryptUtil.sign(keyPairX25519App2.publicKey.bytes, keyPairEd25519App2); 36 | 37 | // User A验证签名 38 | var publicKeyEd25519App2 = base64.encode(keyPairEd25519App2.publicKey.bytes); 39 | var publicKeyX25519App2 = base64.encode(keyPairX25519App2.publicKey.bytes); 40 | var isMatch1 = await EncryptUtil.verify(publicKeyEd25519App2, publicKeyX25519App2, signature2); 41 | assert(isMatch1); 42 | // User B验证签名 43 | var publicKeyEd25519App1 = base64.encode(keyPairEd25519App1.publicKey.bytes); 44 | var publicKeyX25519App1 = base64.encode(keyPairX25519App1.publicKey.bytes); 45 | var isMatch2 = await EncryptUtil.verify(publicKeyEd25519App1, publicKeyX25519App1, signature1); 46 | assert(isMatch2); 47 | 48 | // User A 生成共享密钥 49 | var commomKey1 = await EncryptUtil.sharedSecret(keyPairX25519App1.privateKey, publicKeyX25519App2); 50 | L.d('app1 计算共享密钥: ${base64.encode(sc1)}'); 51 | // User B 生成共享密钥 52 | var commomKey2 = await EncryptUtil.sharedSecret(keyPairX25519App2.privateKey, publicKeyX25519App1); 53 | L.d('app2 计算共享密钥: ${base64.encode(sc2)}'); 54 | assert(base64.encode(commomKey1) == base64.encode(commomKey2)); 55 | 56 | 57 | // User A 加密消息 58 | SecretKey secretKey = SecretKey(base64.decode(commonKey1)); 59 | Nonce nonce = Nonce.randomBytes(12); 60 | List message = utf8.encode('要加密的消息'); 61 | Uint8List encrypted = await EncryptUtil.encrypt(message, secretKey, nonce); 62 | // 发送加密消息 63 | String nonceString = nonce.bytes.toString(); 64 | String body = base64Encode(encrypted); 65 | sendMsg(body, nonceString); 66 | 67 | // User B 解密 68 | SecretKey secretKey = SecretKey(base64.decode(commonKey2)); 69 | Nonce nonce = await getNonceByString(nonceString); 70 | Uint8List decrypted = await EncryptUtil.decrypt(base64Decode(body), secretKey, nonce); 71 | // 打印明文 72 | String result = utf8.decode(decrypted); 73 | } 74 | ``` 75 | 3. 元子操作 76 | 77 | ```dart 78 | class EncryptUtil { 79 | // 根据本地密钥和远程公钥,计算出共享密钥 80 | static Future> sharedSecret(PrivateKey localPrivateKey, String remotePublicKey) async { 81 | PublicKey pk = PublicKey(base64.decode(remotePublicKey)); 82 | // We can now calculate a shared 256-bit secret 83 | SecretKey sharedSecret = await x25519.sharedSecret( 84 | localPrivateKey: localPrivateKey, 85 | remotePublicKey: pk, 86 | ); 87 | 88 | List secretBytes = await sharedSecret.extract(); 89 | return secretBytes; 90 | } 91 | 92 | /// 用 ED25519 key pair 对 X25519 publicKey 签名 93 | static Future sign(List publicKeyX25519, KeyPair keyPairEd25519) async { 94 | // Sign 95 | Signature signature = await ed25519.sign(publicKeyX25519, keyPairEd25519); 96 | 97 | return base64.encode(signature.bytes); 98 | } 99 | 100 | /// 验证公钥与签名是否匹配 101 | static Future verify(publicKeyEd25519, publicKeyX25519, String signature) async { 102 | PublicKey pk = PublicKey(base64.decode(publicKeyEd25519)); 103 | Signature s2 = Signature(base64.decode(signature), publicKey: pk); 104 | bool isSignatureCorrect = await ed25519.verify(base64.decode(publicKeyX25519), s2); 105 | return isSignatureCorrect; 106 | } 107 | 108 | /// AES-CTR加密消息, 并附加Hmac签名 109 | static Future encrypt(Uint8List byte, SecretKey secretKey, Nonce nonce) async { 110 | CipherWithAppendedMac cipher = CipherWithAppendedMac(aesCtr, Hmac(sha512)); 111 | Uint8List encrypted = await cipher.encrypt( 112 | byte, 113 | secretKey: secretKey, 114 | nonce: nonce, 115 | ); 116 | return encrypted; 117 | } 118 | 119 | /// AES-CTR解密消息, 并验证Hmac签名 120 | static Future decrypt(Uint8List encrypted, SecretKey secretKey, Nonce nonce) async { 121 | CipherWithAppendedMac cipher = CipherWithAppendedMac(aesCtr, Hmac(sha512)); 122 | Uint8List decrypted = await cipher.decrypt( 123 | encrypted, 124 | secretKey: secretKey, 125 | nonce: nonce, 126 | ); 127 | return decrypted; 128 | } 129 | 130 | static Future getNonceByString(String nonceStr) async { 131 | String str = nonceStr.substring(1, nonceStr.length - 1); 132 | List nonceList = str.split(','); 133 | List nonceResult = []; 134 | for (int i = 0; i < nonceList.length; i++) { 135 | nonceResult.add(int.parse(nonceList[i])); 136 | } 137 | Nonce nonce = Nonce(nonceResult); 138 | return nonce; 139 | } 140 | } 141 | ``` 142 | 143 | --- 144 | 完结,撒花🎉 145 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——腾讯点播直播下载.md: -------------------------------------------------------------------------------- 1 | 2 | 线上项目应用运行效果: 3 | http://file.jinxianyun.com/tencentplayer.MP4 4 | 5 | demo apk: 6 | http://file.jinxianyun.com/flutter_tencentplayer_0_11_0.apk 7 | 8 | # 0.Tip 9 | 1. 必须真机 10 | 2. android打release包必须加--no-shrink: flutter build apk --release --no-shrink 11 | 3. 打包混淆配置参考[issue99](https://github.com/qq326646683/flutter_tencentplayer/issues/99#issuecomment-839378426) 12 | 13 | # 1.Setup 14 | ``` 15 | flutter_tencentplayer: ${last_version} 16 | 17 | or 18 | 19 | flutter_tencentplayer: 20 | git: 21 | url: https://github.com/qq326646683/flutter_tencentplayer.git 22 | ``` 23 | > For Android 24 | 25 | 1. project/android/build.gradle 添加依赖的aar: 26 | ```gradle 27 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 28 | def plugins = new Properties() 29 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 30 | if (pluginsFile.exists()) { 31 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 32 | } 33 | 34 | allprojects { 35 | repositories { 36 | google() 37 | jcenter() 38 | flatDir { 39 | dirs "${plugins.get("flutter_tencentplayer")}android/libs" 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | 2. AndroidManifest.xml 声明权限: 46 | ``` 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ``` 65 | 66 | > For Ios 67 | 68 | 69 | 70 | ``` 71 | 72 | //项目的info.plist文件上添加如下权限 73 | NSAppTransportSecurity 74 | 75 | NSAllowsArbitraryLoads 76 | 77 | 78 | ``` 79 | 80 | # 2.Usage(TencentPlayer) 81 | 82 | > 和video_player api相似,支持直播源,视频跳转,切换视频源,边下边播放,清晰度切换,设置播放速度 83 | 84 | ### 1.初始化播放器,支持asset、network、filePath、fileId四种方式播放 85 | ```dart 86 | TencentPlayerController _controller; 87 | 88 | _MyAppState() { 89 | listener = () { 90 | if (!mounted) { 91 | return; 92 | } 93 | setState(() {}); 94 | }; 95 | } 96 | 97 | initState() { 98 | _controller = TencentPlayerController.network('http://file.jinxianyun.com/testhaha.mp4', playerConfig: PlayerConfig()) 99 | //_controller = TencentPlayerController.asset('static/tencent1.mp4') 100 | //_controller = TencentPlayerController.file('/storage/emulated/0/test.mp4') 101 | //_controller = TencentPlayerController.network(null, playerConfig: {auth: {"appId": 1252463788, "fileId": '4564972819220421305'}}) 102 | ..initialize().then((_) { 103 | setState(() {}); 104 | }); 105 | _controller.addListener(listener); 106 | } 107 | ``` 108 | 109 | ### 2.PlayerConfig (播放器配置参数 ) 110 | ```dart 111 | _controller = TencentPlayerController.network(url, playerConfig: PlayerConfig()) 112 | 113 | ``` 114 | 115 | Prop | Type | Default | Note 116 | ---|---|---|--- 117 | autoPlay | bool | true | 是否自动播放 118 | loop | bool | false | 是否循环播放 119 | headers | Map | | 请求头 120 | cachePath | String | | 缓存路径(边播放边下载) 121 | progressInterval | int | 1 | 播放进度回调频率(秒) 122 | startTime | int | 0 | 哪里开始播放(秒) 123 | auth | Map | | 云点播视频源appId&fileId&sign 124 | supportBackground | bool | false | 是否后台播放 125 | 126 | 127 | 128 | ### 3.TencentPlayerValue (播放器回调) 129 | ```dart 130 | Text("总时长:" + _controller.value.duration.toString()) 131 | ``` 132 | Prop | Type | Note 133 | ---|---|--- 134 | initialized | bool | 是否初始化完成从而显示播放器 135 | aspectRatio | double | 用来控制播放器宽高比 136 | duration | Duration | 时长 137 | position | Duration | 播放进度 138 | playable | Duration | 缓冲进度 139 | isPlaying | bool | 是否在播放 140 | size | Size | 视频宽高 141 | isLoading | bool | 是否在加载 142 | netSpeed | int | 视频播放网速 143 | rate | double | 播放速度 144 | bitrateIndex | int | 视频清晰度 145 | orientation | int | 手机旋转角度(android only) 146 | degree | int | 本地file视频自带旋转属性 147 | eventCode | int | 事件监听[code](https://cloud.tencent.com/document/product/881/20216#.E4.BA.8B.E4.BB.B6.E7.9B.91.E5.90.AC) 148 | 149 | ### 4.Event (播放器事件) 150 | 151 | a.跳转进度 152 | ```dart 153 | _controller.seekTo(Duration(seconds: 5)); 154 | 155 | ``` 156 | b.设置播放速度 157 | ```dart 158 | _controller.setRate(1.5); // 1.0 ~ 2.0 159 | 160 | ``` 161 | c.切换播放源 162 | ```dart 163 | controller?.removeListener(listener); 164 | controller?.pause(); 165 | controller = TencentPlayerController.network(url, playerConfig: PlayerConfig(startTime: startTime ?? controller.value.position.inSeconds)); 166 | controller?.initialize().then((_) { 167 | if (mounted) setState(() {}); 168 | }); 169 | controller?.addListener(listener); 170 | ``` 171 | d.切换清晰度(实质就是切换播放源) 172 | 173 | 174 | # 3.Usage(Download) 175 | > 离线下载, 支持断点续传(这里只支持m3u8视频、fileId), 支持多文件同时下载 176 | 177 | ### 1.初始化下载器 178 | 179 | ```dart 180 | DownloadController _downloadController; 181 | 182 | _MyAppState() { 183 | downloadListener = () { 184 | if (!mounted) { 185 | return; 186 | } 187 | setState(() {}); 188 | }; 189 | } 190 | 191 | initState() { 192 | _downloadController = DownloadController('/storage/emulated/0/tencentdownload', appId: 1252463788); 193 | _downloadController.addListener(downloadListener); 194 | } 195 | ``` 196 | ### 2.Event (下载事件) 197 | 198 | a. 下载 199 | ```dart 200 | _downloadController.dowload("4564972819220421305", quanlity: 2); 201 | // _downloadController.dowload("http://1253131631.vod2.myqcloud.com/26f327f9vodgzp1253131631/f4bdff799031868222924043041/playlist.m3u8"); 202 | ``` 203 | b. 暂停下载 204 | ```dart 205 | _downloadController.pauseDownload("4564972819220421305"); 206 | // _downloadController.stopDownload("http://1253131631.vod2.myqcloud.com/26f327f9vodgzp1253131631/f4bdff799031868222924043041/playlist.m3u8"); 207 | 208 | ``` 209 | b. 取消下载 210 | ```dart 211 | _downloadController.cancelDownload("4564972819220421305"); 212 | // _downloadController.cancelDownload("http://1253131631.vod2.myqcloud.com/26f327f9vodgzp1253131631/f4bdff799031868222924043041/playlist.m3u8"); 213 | 214 | ``` 215 | 216 | 217 | ### 3.DownloadValue (下载信息回调) 218 | >因为支持多文件同时下载,回调以Map返回,key为url/fileId 219 | 220 | Prop | Type | Note 221 | ---|---|--- 222 | downloadStatus | String | "start"、"progress"、"stop"、"complete"、"error" 223 | quanlity | int | 1: "FLU"、2: "SD"、3: "HD"、4: "FHD"、5: "2K"、6: "4K" 224 | duration | int | 225 | size | int | 文件大小 226 | downloadSize | int | 已下载大小 227 | progress | int | 已下载大小 228 | playPath | String | 下载文件的绝对路径 229 | isStop | bool | 是否暂停下载 230 | url | String | 下载的视频链接 231 | fileId | String | 下载的视频FileId 232 | error | String | 下载的错误信息 233 | 234 | 235 | 236 | # 4.[Example](https://github.com/qq326646683/flutter_tencentplayer/blob/master/example/lib/main.dart) 237 | 238 | # 5.Note 239 | > 1. flutter1.10+ android打包命令: 240 | ``` 241 | flutter build apk --release --no-shrink 242 | ``` 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——苹果内购.md: -------------------------------------------------------------------------------- 1 | > ### 一、以下是消耗类和非消耗类的正常流程(订阅类的不太清楚) 2 | - 1.进入充值页面,向app server获取productIdList及展示信息。 3 | - 2.用productIdList调iapsdk获取productDetailList(用来发起支付的参数)。 4 | - 3.用户选择一个productDetail,然后调iap sdk发起支付。 5 | - 4.监听到apple支付成功,将purshaseId、receipt发给app server。 6 | - 5.app server 向apple server发起校验请求,比对in_app数组里对应的purshaseId的校验结果,返回给第4步app的请求。 7 | - 6.app端收到成功结果后finish掉该productDetail。 8 | 9 | > ### 二、以下是应对异常情况 10 | - a.上面第4步断网或者app闪退。 11 | - b.上面第6步因为网络原因没有finish该productDetail。 12 | ###### 针对第a种异常: 13 | - i.下次进入app用iap sdk获取未处理(未finish)栈里的productDetail(这个flutter iap plugin没有提供方法,我自己fork后加了[该方法](https://github.com/qq326646683/plugins))(注意这里是单数,只有一个未处理),然后接着走正常流程的4、5、6。 14 | - ii.再次购买时,先执行i的步骤,确保处理完毕了才能发起第二笔支付,否则获取未处理productDetail为复数时会导致receipt紊乱导致校验失败造成卡单(未处理栈里一直在)。 15 | ###### 针对第b种异常: 16 | - a的异常处理会让app server重复校验,所以这里需要app server做一下记录,校验过的结果存在数据库里,再发起该purshaseId校验直接返回结果,避免重复增加余额。 17 | 18 | > ### 三、编码参考 19 | 20 | 环境: 21 | ``` 22 | flutter版本: v1.9.1+hotfix.4 23 | 插件依赖:in_app_purchase: 24 | git: 25 | url: https://github.com/qq326646683/plugins.git 26 | ref: 13df320b6112a3a4abfbec47bba53b2f95402637 27 | path: packages/in_app_purchase 28 | ``` 29 | balance_page.dart: 30 | ``` dart 31 | @override 32 | void initState() { 33 | InappPurchaseService.getInstance().initListener(context); 34 | super.initState(); 35 | /// 步骤1 36 | ResponseResult> response = await OrderService.getInstance().getAppleProduct(); 37 | if (response.isSuccess) { 38 | /// 步骤2 39 | _initStoreInfo(response.data); 40 | } 41 | } 42 | 43 | @override 44 | void dispose() { 45 | InappPurchaseService.getInstance().removeListener(); 46 | super.dispose(); 47 | } 48 | 49 | _initStoreInfo(List appProductList) async { 50 | productDetailList = await InappPurchaseService.getInstance().initStoreInfo(context, appProductList); 51 | } 52 | 53 | build() { 54 | ... 55 | SMClickButton( 56 | /// 步骤3 57 | onTap: () => InappPurchaseService.getInstance().toCharge(productDetailList, selectIndex, context), 58 | child: Container( 59 | width: _Style.btnContainerW, 60 | height: _Style.bottomContainer, 61 | color: SMColors.btnColorfe373c, 62 | alignment: Alignment.center, 63 | child: Text('确认充值', style: SMTxtStyle.colorfffdp16,), 64 | ), 65 | ), 66 | } 67 | 68 | 69 | ``` 70 | 71 | inapp_purchase_service.dart: 72 | 73 | ```dart 74 | 75 | initListener(BuildContext context) { 76 | final Stream purchaseUpdates = InAppPurchaseConnection.instance.purchaseUpdatedStream; 77 | _subscription = purchaseUpdates.listen((purchases) { 78 | _listenToPurchaseUpdated(context, purchases); 79 | }, onDone: () => _subscription.cancel(), onError: (error) => LogUtil.i(InappPurchaseService.sName, "error:" + error)); 80 | 81 | } 82 | void _listenToPurchaseUpdated(BuildContext context, List purchaseDetailsList) { 83 | purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { 84 | if (purchaseDetails.status == PurchaseStatus.pending) { 85 | LogUtil.i(InappPurchaseService.sName, 'PurchaseStatus.pending'); 86 | LoadingUtil.show(context); 87 | } else { 88 | LoadingUtil.hide(); 89 | 90 | if (purchaseDetails.status == PurchaseStatus.error) { 91 | ToastUtil.showRed("交易取消或失败"); 92 | } else if (purchaseDetails.status == PurchaseStatus.purchased) { 93 | ToastUtil.showGreen("交易成功,正在校验"); 94 | LogUtil.i(InappPurchaseService.sName, "purchaseDetails.purchaseID:" + purchaseDetails.purchaseID); 95 | LogUtil.i(InappPurchaseService.sName, "purchaseDetails.serverVerificationData:" + purchaseDetails.verificationData.serverVerificationData); 96 | /// 步骤4 97 | _verifyPurchase(purchaseDetails, needLoadingAndToast: true, context: context); 98 | } 99 | } 100 | }); 101 | } 102 | 103 | // return bool needLock 104 | Future _verifyPurchase(PurchaseDetails purchaseDetails, {bool needLoadingAndToast = false, BuildContext context}) async { 105 | Map param = { 106 | "transactionId" : purchaseDetails.purchaseID, 107 | "receipt": purchaseDetails.verificationData.serverVerificationData, 108 | }; 109 | if (needLoadingAndToast) LoadingUtil.show(context); 110 | ResponseResult response = await ZBDao.charge(param); 111 | if (needLoadingAndToast) LoadingUtil.hide(); 112 | if (response.isSuccess) { 113 | if (response.data == true) { 114 | /// 步骤6 115 | await InAppPurchaseConnection.instance.completePurchase(purchaseDetails); 116 | if (needLoadingAndToast) ToastUtil.showGreen('充值成功'); 117 | OrderService.getInstance().getBalance(); 118 | return false; 119 | } else { 120 | if (needLoadingAndToast) ToastUtil.showRed('充值失败'); 121 | return true; 122 | } 123 | } else { 124 | LogUtil.i(InappPurchaseService.sName, '处理失败'); 125 | return true; 126 | } 127 | } 128 | 129 | 130 | Future> initStoreInfo(BuildContext context, List appleProductList) async { 131 | final bool isAvailable = await _connection.isAvailable(); 132 | if (!isAvailable) { 133 | return null; 134 | } 135 | 136 | List productIdList = []; 137 | 138 | for(AppleProduct appleProduct in appleProductList) { 139 | productIdList.add(appleProduct.productId); 140 | } 141 | 142 | LoadingUtil.show(context); 143 | ProductDetailsResponse productDetailResponse = await _connection.queryProductDetails(productIdList.toSet()); 144 | LoadingUtil.hide(); 145 | 146 | if (productDetailResponse.error != null) { 147 | return null; 148 | } 149 | 150 | if (productDetailResponse.productDetails.isEmpty) { 151 | return null; 152 | } 153 | 154 | return productDetailResponse.productDetails; 155 | } 156 | 157 | 158 | void toCharge(List productDetailList, int selectIndex, BuildContext context) async { 159 | if (productDetailList == null) { 160 | ToastUtil.showRed("productDetailList为空"); 161 | return; 162 | } 163 | LoadingUtil.show(context); 164 | /// a异常ii步骤 165 | bool needLock = await checkUndealPurshase(); 166 | LoadingUtil.hide(); 167 | if (needLock) { 168 | ToastUtil.showRed("有订单未处理"); 169 | return; 170 | } 171 | 172 | final PurchaseParam purchaseParam = PurchaseParam(productDetails:productDetailList[selectIndex]); 173 | 174 | InAppPurchaseConnection.instance.buyConsumable(purchaseParam: purchaseParam); 175 | } 176 | 177 | // return bool needLock 178 | Future checkUndealPurshase() async { 179 | /// a.异常i步骤,这里在进入app后,用户获取登录状态后调用 180 | LogUtil.i(InappPurchaseService.sName, '获取未处理list'); 181 | try { 182 | List purchaseDetailsList = await _connection.getUndealPurchases(); 183 | if (purchaseDetailsList.isEmpty) return false; 184 | LogUtil.i(InappPurchaseService.sName, '处理数组最后一个'); 185 | PurchaseDetails purchaseDetails = purchaseDetailsList[purchaseDetailsList.length - 1]; 186 | return _verifyPurchase(purchaseDetails); 187 | } catch(e) { 188 | ToastUtil.showRed('同步苹果支付信息失败'); 189 | return true; 190 | } 191 | } 192 | 193 | ``` 194 | 195 | --- 196 | 完结,撒花🎉 197 | -------------------------------------------------------------------------------- /flutter/Flutter上线项目实战——路由篇.md: -------------------------------------------------------------------------------- 1 | ## 1. 应用场景 2 | >开发中经常遇到 3 | 4 | - 路由跳转时拿不到context怎么办,eg: token失效/异地登录跳转登录页面。 5 | - 获取不到当前路由名称怎么办,eg: 点击push推送跳转指定路由,如果已经在当前页面就replace,如果不在就push。 6 | - 注册监听路由跳转,做一些想做的事情,eg:不同路由,显示不同状态栏颜色。 7 | - 监听当前页面获取、失去焦点 8 | - 等等... 9 | 10 | 11 | ## 2. 解决方案 12 | >解决思路: 13 | 14 | 1. MaterialApp 的routes属性赋值路由数组,navigatorObservers属性赋值路由监听对象NavigationUtil。 15 | 2. 在NavigationUtil里实现NavigatorObserver的didPush/didReplace/didPop/didRemove,并记录到路由栈 16 | List _mRoutes中。 17 | 3. 将实时记录的路由跳转,用stream发一个广播,哪里需要哪里注册。 18 | 4. 用mixin实现当前页面获取、失去焦点,监听当前路由变化,触发onFocus,onBlur。 19 | 20 | 21 | ## 3. 具体实现 22 | >支持空安全版本 [戳我](https://github.com/qq326646683/flutter_collection_demo/tree/main/lib/util/navigation) 23 | 24 | >main.dart 25 | 26 | ``` dart 27 | MaterialApp( 28 | navigatorObservers: [NavigationUtil.getInstance()], 29 | routes: NavigationUtil.configRoutes, 30 | ... 31 | ) 32 | ``` 33 | >navigation_util.dart 34 | 35 | ``` dart 36 | class RouteInfo { 37 | Route currentRoute; 38 | List routes; 39 | 40 | RouteInfo(this.currentRoute, this.routes); 41 | 42 | @override 43 | String toString() { 44 | return 'RouteInfo{currentRoute: $currentRoute, routes: $routes}'; 45 | } 46 | } 47 | 48 | class NavigationUtil extends NavigatorObserver { 49 | static NavigationUtil _instance; 50 | 51 | static Map configRoutes = { 52 | SplashPage.sName: (_) => SplashPage(), 53 | ScanQrPage.sName: (_) => ScanQrPage(), 54 | }; 55 | 56 | ///路由信息 57 | RouteInfo _routeInfo; 58 | RouteInfo get routeInfo => _routeInfo; 59 | ///stream相关 60 | static StreamController _streamController; 61 | StreamController get streamController=> _streamController; 62 | ///用来路由跳转 63 | static NavigatorState navigatorState; 64 | 65 | 66 | static NavigationUtil getInstance() { 67 | if (_instance == null) { 68 | _instance = new NavigationUtil(); 69 | _streamController = StreamController.broadcast(); 70 | } 71 | return _instance; 72 | } 73 | 74 | ///push页面 75 | Future pushNamed(String routeName, {WidgetBuilder builder, bool fullscreenDialog}) { 76 | return navigatorState.push( 77 | MaterialPageRoute( 78 | builder: builder ?? configRoutes[routeName], 79 | settings: RouteSettings(name: routeName), 80 | fullscreenDialog: fullscreenDialog ?? false, 81 | ), 82 | ); 83 | } 84 | 85 | ///replace页面 86 | Future pushReplacementNamed(String routeName, {WidgetBuilder builder, bool fullscreenDialog}) { 87 | return navigatorState.pushReplacement( 88 | MaterialPageRoute( 89 | builder: builder ?? configRoutes[routeName], 90 | settings: RouteSettings(name: routeName), 91 | fullscreenDialog: fullscreenDialog ?? false, 92 | ), 93 | ); 94 | } 95 | 96 | /// pop 页面 97 | pop([T result]) { 98 | navigatorState.pop(result); 99 | } 100 | 101 | pushNamedAndRemoveUntil(String newRouteName) { 102 | return navigatorState.pushNamedAndRemoveUntil(newRouteName, (Route route) => false); 103 | } 104 | 105 | @override 106 | void didPush(Route route, Route previousRoute) { 107 | super.didPush(route, previousRoute); 108 | if (_routeInfo == null) { 109 | _routeInfo = new RouteInfo(null, new List()); 110 | } 111 | ///这里过滤调push的是dialog的情况 112 | if (route is CupertinoPageRoute || route is MaterialPageRoute) { 113 | _routeInfo.routes.add(route); 114 | routeObserver(); 115 | } 116 | } 117 | 118 | @override 119 | void didReplace({Route newRoute, Route oldRoute}) { 120 | super.didReplace(); 121 | if (newRoute is CupertinoPageRoute || newRoute is MaterialPageRoute) { 122 | _routeInfo.routes.remove(oldRoute); 123 | _routeInfo.routes.add(newRoute); 124 | routeObserver(); 125 | } 126 | } 127 | 128 | @override 129 | void didPop(Route route, Route previousRoute) { 130 | super.didPop(route, previousRoute); 131 | if (route is CupertinoPageRoute || route is MaterialPageRoute) { 132 | _routeInfo.routes.remove(route); 133 | routeObserver(); 134 | } 135 | } 136 | 137 | @override 138 | void didRemove(Route removedRoute, Route oldRoute) { 139 | super.didRemove(removedRoute, oldRoute); 140 | if (removedRoute is CupertinoPageRoute || removedRoute is MaterialPageRoute) { 141 | _routeInfo.routes.remove(removedRoute); 142 | routeObserver(); 143 | } 144 | } 145 | 146 | 147 | 148 | void routeObserver() { 149 | _routeInfo.currentRoute = _routeInfo.routes.last; 150 | navigatorState = _routeInfo.currentRoute.navigator; 151 | _streamController.sink.add(_routeInfo); 152 | } 153 | } 154 | ``` 155 | >navigation_mixin.dart 156 | 157 | ``` dart 158 | mixin NavigationMixin on State { 159 | StreamSubscription streamSubscription; 160 | Route lastRoute; 161 | 162 | @override 163 | void initState() { 164 | super.initState(); 165 | 166 | streamSubscription = NavigationUtil.getInstance().streamController.stream.listen((RouteInfo routeInfo) { 167 | if (routeInfo.currentRoute.settings.name == routName) { 168 | onFocus(); 169 | } 170 | /// 第一次监听到路由变化 171 | if (lastRoute == null) { 172 | onBlur(); 173 | } 174 | /// 上一个是该页面,新的路由不是该页面 175 | if (lastRoute?.settings?.name == routName && routeInfo.currentRoute.settings.name != routName) { 176 | onBlur(); 177 | } 178 | lastRoute = routeInfo.currentRoute; 179 | 180 | }); 181 | } 182 | 183 | @override 184 | void dispose() { 185 | super.dispose(); 186 | streamSubscription?.cancel(); 187 | streamSubscription = null; 188 | } 189 | 190 | @protected 191 | String get routName; 192 | 193 | @protected 194 | void onBlur() { 195 | 196 | } 197 | 198 | @protected 199 | void onFocus() { 200 | 201 | } 202 | } 203 | ``` 204 | 205 | ## 4. 如何使用 206 | >token失效跳转 207 | 208 | ``` dart 209 | case 401: 210 | ToastUtil.showRed('登录失效,请重新登陆'); 211 | UserDao.clearAll(); 212 | NavigationUtil.getInstance().pushNamedAndRemoveUntil(LoginPage.sName); 213 | break; 214 | ``` 215 | 216 | >点击push推送跳转 217 | 218 | ``` dart 219 | static jumpPage(String pageName, [WidgetBuilder builder]) { 220 | String currentRouteName = NavigationUtil.getInstance().currentRoute.settings.name; 221 | // 如果是未登录,不跳转 222 | if (NavigationUtil.getInstance().routes[0].settings.name != MainPage.sName) { 223 | return; 224 | } 225 | 226 | // 如果已经是当前页面就replace 227 | if (currentRouteName == pageName) { 228 | NavigationUtil.getInstance().pushReplacementNamed(pageName, builder); 229 | } else { 230 | NavigationUtil.getInstance().pushNamed(pageName, builder); 231 | } 232 | } 233 | ``` 234 | >监听路由改变状态栏颜色 235 | 236 | ``` dart 237 | class StatusBarUtil { 238 | static List lightRouteNameList = [ 239 | TaskhallPage.sName, 240 | //... 241 | ]; 242 | static List darkRoutNameList = [ 243 | SplashPage.sName, 244 | LoginPage.sName, 245 | MainPage.sName, 246 | //... 247 | ]; 248 | 249 | static init() { 250 | NavigationUtil.getInstance().streamController.stream.listen((state) { 251 | setupStatusBar(state[state.length - 1]); 252 | }) 253 | } 254 | 255 | setupStatusBar(Route currentRoute) { 256 | if (lightRouteNameList.contains(currentRoute.settings.name)) { 257 | setLight(); 258 | } else if (darkRoutNameList.contains(currentRoute.settings.name)) { 259 | setDart(); 260 | } 261 | } 262 | } 263 | ``` 264 | >当前页面获取、失去焦点 265 | 266 | ``` dart 267 | class _ChatPageState extends State with NavigationMixin { 268 | ... 269 | @override 270 | String get routName => ChatPage.sName; 271 | 272 | @override 273 | void onBlur() { 274 | super.onBlur(); 275 | // do something 276 | } 277 | 278 | @override 279 | void onFocus() { 280 | super.onFocus(); 281 | // do something 282 | } 283 | } 284 | 285 | ``` 286 | 287 | 288 | --- 289 | ##### 完结,撒花🎉 290 | 291 | 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /android/Android学习——Handler通信机制.md: -------------------------------------------------------------------------------- 1 | ### 1. 作用 2 | > 先来思考一个问题,android线程间内存是共享的,为什么我们还需要Handler传递消息? 3 | 4 | 1. 为了UI渲染不卡顿,需要将UI渲染和耗时任务放在不同线程中执行,互不干扰保证UI渲染的流畅性。 5 | 2. 为了做到第1点,Android默认将UI渲染限制在UI线程中执行 6 | ```java 7 | // ViewRootImpl.java: 8 | 9 | @Override 10 | public void requestLayout() { 11 | if (!mHandlingLayoutInLayoutRequest) { 12 | checkThread(); 13 | mLayoutRequested = true; 14 | scheduleTraversals(); 15 | } 16 | } 17 | 18 | void checkThread() { 19 | if (mThread != Thread.currentThread()) { 20 | throw new CalledFromWrongThreadException( 21 | "Only the original thread that created a view hierarchy can touch its views."); 22 | } 23 | } 24 | ``` 25 | 可以看到在执行requestLayout时检查了是否在UI线程 26 | 27 | > 如果没有Handler我们该怎么做? 28 | 3. 限于1和2的背景,应用到我们实际场景中去,假如我们在子线程做完耗时任务后,将数据直接赋值给UI线程中,这个时候UI线程并不知道该值发生了变化,于是我们写一个Listener通知主线程,像这样: 29 | ```kotlin 30 | var test = 1 31 | private var listener = object: TestCallback { 32 | override fun call(t: Int) { 33 | // 当前线程: 子线程 34 | Log.d("当前线程:", Thread.currentThread().toString()) 35 | } 36 | } 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | findViewById