├── .gitignore ├── README.md ├── build.gradle ├── config.gradle ├── dependencies.gradle ├── easytrack ├── .gitignore ├── README ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── pengxr │ │ └── easytrack │ │ ├── core │ │ ├── EasyTrack.kt │ │ ├── FillStrategy.kt │ │ ├── IPageTrackNode.kt │ │ ├── ITrackModel.kt │ │ ├── ITrackNode.kt │ │ ├── ITrackProvider.kt │ │ ├── TrackModel.kt │ │ └── TrackParams.kt │ │ ├── impl │ │ ├── BaseTrackActivity.kt │ │ └── BaseTrackFragment.kt │ │ ├── session │ │ ├── TrackEvent.kt │ │ └── TrackSession.kt │ │ └── util │ │ ├── EasyTrackUtils.kt │ │ ├── LogUtil.kt │ │ └── ViewUtils.kt │ └── res │ └── values │ └── ids.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── EasyTrack - 困难.png ├── EasyTrack - 流程.png ├── EasyTrack - 目录.png ├── EasyTrack - 西瓜1.png └── EasyTrack - 西瓜2.png ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── pengxr │ │ └── sample │ │ ├── base │ │ ├── BaseActivity.kt │ │ └── MyApplication.kt │ │ ├── entity │ │ ├── GoodsItem.kt │ │ └── StoreDetail.kt │ │ ├── goods │ │ ├── view │ │ │ ├── GoodsDetailActivity.java │ │ │ └── GoodsDetailFragment.java │ │ └── vm │ │ │ └── GoodsDetailViewModel.java │ │ ├── statistics │ │ ├── EventConstants.java │ │ ├── SensorProvider.kt │ │ ├── StatisticsUtils.kt │ │ └── UmengProvider.kt │ │ ├── store │ │ ├── view │ │ │ ├── StoreHomeActivity.kt │ │ │ ├── StoreNewestFragment.kt │ │ │ └── StoreRecommendFragment.kt │ │ ├── vm │ │ │ └── StoreHomeViewModel.kt │ │ └── widget │ │ │ └── GoodsViewHolder.kt │ │ ├── utils │ │ ├── DensityUtil.java │ │ ├── ToastUtil.java │ │ └── VMCompat.java │ │ └── widget │ │ ├── CircleImageView.java │ │ ├── HackyViewPager.java │ │ ├── HeightAutoFitSquareImageView.java │ │ ├── ImmersiveStatusBarSpace.java │ │ └── SpacingDecoration.java │ └── res │ ├── drawable-xhdpi │ ├── ic_back.png │ └── ic_share.png │ ├── drawable-xxhdpi │ ├── ic_back.png │ ├── ic_launcher.jpg │ ├── ic_share.png │ ├── icon_baozi.png │ ├── icon_bike.png │ ├── icon_cat.png │ ├── icon_guita.png │ ├── icon_icecream.png │ └── icon_noodle.jpg │ ├── layout │ ├── goods_detail_activity.xml │ ├── goods_detail_fragment.xml │ ├── layout_fragment.xml │ ├── layout_title.xml │ ├── store_home_activity.xml │ └── store_home_goods_item.xml │ ├── values-night │ └── themes.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── strings.xml │ └── themes.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/pengxurui/AndroidFamily/blob/master/images/Android_Banner.png) 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 | 12 | 13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |

39 | 40 | 41 | # 前言 42 | 43 | - 目前,几乎每个商用应用都有数据埋点的需求。你的 App 是怎么做埋点的呢,有遇到让你 “难顶” 的问题吗? 44 | - 在这篇文章里,我将带你建立数据埋点的基本认识,还会介绍西瓜视频团队的前端埋点方案,最后为你带来我的落地实现 EasyTrack。 45 | 46 | --- 47 | # 目录 48 | 49 | ![](https://github.com/pengxurui/EasyTrack/blob/master/images/EasyTrack%20-%20%E7%9B%AE%E5%BD%95.png) 50 | 51 | --- 52 | # 1. 数据埋点概述 53 | 54 | #### 1.1 为什么要埋点? 55 | 56 | “除了上帝,任何人都必须用数据说话”,在数据时代,使用数据驱动产品迭代已经称为行业共识。在分析应用数据之前,首先需要获得数据,这就需要前端或服务端进行数据埋点。 57 | 58 | #### 1.2 数据需求的工作流程 59 | 60 | 首先,你需要了解数据需求的工作流程,需求是如何产生,又是如何流转的,主要分为以下几个环节: 61 | 62 | - **1、需求产生:** 产品需求引起产品形态变化,产生新的数据需求; 63 | - **2、事件设计:** 数据产品设计埋点事件并更新数据字典文档,提出埋点评审; 64 | - **3、埋点开发:** 开发进行数据埋点开发; 65 | - **4、埋点测试:** 测试进行数据埋点测试,确保数据质量; 66 | - **5、数据消费:** 数据分析师进行数据分析,推荐系统工程师进行模型训练,赋能产品运营决策。 67 | 68 | ![](https://github.com/pengxurui/EasyTrack/blob/master/images/EasyTrack%20-%20%E6%B5%81%E7%A8%8B.png) 69 | 70 | #### 1.3 数据消费的经典场景 71 | 72 | |消费场景|需求描述|技术需求| 73 | |:---|:---|:---| 74 | |**渗透率分析**|统计 DAU/PV/UV/VV 等|准确的上报时机| 75 | |**归因分析**|分析前因后果|准确上报上下文 (如场景、会话、来源页面)| 76 | |**1. A / B 测试
2. 个性化推荐**|分析用户特征、产品特征等|准确上报事件属性| 77 | 78 | 可以看到,在归因分析中,除了需要上报事件本身的属性之外,还需要上报事件产生时的上下文信息,例如当前页面、来源页面、会话等。 79 | 80 | #### 1.4 埋点数据采集的基本模型 81 | 82 | 数据采集是指在前端或服务端收集需要上报的事件属性的过程。为了满足复杂、高效的数据消费需求,需要科学合理地设计端侧的数据采集逻辑,基本可以总结为 “4W + 1H” 模型: 83 | 84 | |模型|描述|举例| 85 | |:---|:---|:---| 86 | |**1、WHAT**|什么行为|事件名| 87 | |**2、WHEN**|行为产生的时间|时间戳| 88 | |**3、WHO** |行为产生的对象|对象唯一标识 (例如用户 ID、设备 ID)| 89 | |**4、WHERE**|行为产生的环境|设备所处的环境 (例如 IP、操作系统、网络)| 90 | |**5、HOW**|行为的特征|上下文信息 (例如当前页面、来源页面、会话)| 91 | 92 | --- 93 | # 2. 如何实现数据埋点? 94 | 95 | #### 2.1 埋点方案总结 96 | 97 | 目前,业界已经存在多种埋点方案,主要分为全埋点、前端代码埋点和服务端代码埋点三种,优缺点和适用场景总结如下: 98 | 99 | ||全埋点|前端埋点|服务端埋点| 100 | |:---|:---|:---|:---| 101 | |优势|开发成本低|完整采集上下文信息|不依赖于前端版本| 102 | |劣势|数据量大,无法获取上下文数据,数据质量低|前端开发成本较高|服务端开发成本较高、获取上下文信息依赖于接口传值| 103 | |适用场景|通用基础事件(如启动/退出、浏览、点击)|核心业务流程(如登录、注册、收藏、购买)|核心业务结果事件(如支付成功)| 104 | 105 | - **1、全埋点:** 指通过编译时插桩、运行时动态代理等 AOP 手段实现自动埋点和上报,无须开发者手动进行埋点,因此也称为 “无埋点”; 106 | 107 | - **2、前端埋点:** 指前端 (包括客户端) 开发者手动编码实现埋点,虽然可以通过埋点工具或者脚本简化埋点开发工作,但总体上还是需要手动操作; 108 | 109 | - **3、服务端埋点:** 指服务端手动编码实现埋点,缺点是需要客户端需要侵入接口来保留上下文参数。 110 | 111 | #### 2.2 全埋点方案的局限性 112 | 113 | 表面上看,全埋点方案的优势很明显:客户端和服务端只需要一次开发,就能实现所有页面、所有路径的曝光和点击事件埋点,节省了研发人力,也不用担心埋点逻辑会侵入正常业务逻辑。然而,不可能存在完美的解决方案,全埋点方案还是存在一些局限性: 114 | 115 | - **1、资源消耗较大:** 全场景上报会产生大量无用数据,网络传输、数据存储和数据计算需要消耗大量资源; 116 | 117 | - **2、页面稳定性要求较高:** 需要保持页面视图结构相对稳定,一旦页面视图结果变化,历史录入的埋点数据就会失效; 118 | 119 | - **3、无法采集上下文信息:** 无法采集事件产生时的上下文信息,也就无法满足复杂的数据消费需求。 120 | 121 | #### 2.3 埋点设计的整体方案 122 | 123 | 考虑的不同方案都存在优缺点,单纯采用一种埋点方案是不切实际的,需要根据不同业务场景和不同数据消费需要而采用不同的埋点方案: 124 | 125 | - **1、全埋点:** 作为全局兜底方案,可以满足粗粒度的统计需求; 126 | 127 | - **2、前端埋点:** 作为全埋点的补充方案,可以自定义埋点参数,主要处理核心业务流程事件,例如(如登录、注册、收藏、购买); 128 | 129 | - **3、服务端埋点:** 核心业务结果事件,例如订单支付成功。 130 | 131 | --- 132 | # 3. 前端埋点中的困难 133 | 134 | #### 3.1 一个简单的埋点场景 135 | 136 | 现在,我们通过一个具体的埋点场景,试着发现在做埋点需求时会遇到的困难或痛点。我直接使用西瓜视频中的一个埋点场景: 137 | 138 | ![](https://github.com/pengxurui/EasyTrack/blob/master/images/EasyTrack%20-%20%E5%9B%B0%E9%9A%BE.png) 139 | 140 | —— 图片引用自西瓜视频技术博客 141 | 142 | 这个产品场景很简单,左边是西瓜视频的推荐流列表,点击 “电影卡片” 会进入右边的 “电影详情页” 。**两个页面中都有 “收藏按钮”,现在的数据需求是采集不同页面中 “收藏按钮” 的点击事件,以便分析用户收藏影片的行为,优化影片的推荐模型。** 143 | 144 | - **1、在推荐列表页中上报点击事件:** 145 | ``` 146 | “event_name" : "click_favorite", // 事件名 147 | "cur_page" : "feed", // 当前页面 148 | "video_id" : "123", // 影片 ID 149 | "video_name" : "影片名", // 影片名 150 | "video_type" : "1", // 影片类型 151 | "$user_id" : "10000", // 用户 ID 152 | "$device_id" : "abc" // 设备 ID 153 | ... // 其他预置属性 154 | ``` 155 | 156 | - **2、在电影详情页中上报点击事件:** 157 | ``` 158 | “event_name" : "click_favorite", // 事件名 159 | "from_page" : "feed" 160 | "cur_page" : "video_detail", // 当前页面 161 | "video_id" : "123", // 影片 ID 162 | "video_name" : "影片名", // 影片名 163 | "video_type" : "1", // 影片类型 164 | "$user_id" : "10000", // 用户 ID 165 | "$device_id" : "abc" // 设备 ID 166 | ... // 其他预置属性 167 | ``` 168 | 169 | #### 3.2 现状分析 170 | 171 | 理解了这个埋点场景之后,我们先梳理出目前遇到的困难: 172 | 173 | - **1、埋点参数分散:** 需要上报的埋点参数位于不同 UI 容器或不同业务模块,代码跨度很大(例如:Activity、Fragment、ViewHolder、自定义 View); 174 | 175 | - **2、组件复用:** 组件抽象复用后在多个页面使用(例如通用的 ViewHolder 或自定义 View); 176 | 177 | - **3、数据模型不一致:** 不同场景 / 页面下描述状态的数据模型不一致,需要额外的转换适配过程(例如有的模型用 video_type 表示影片类型,另一些模型用 videoType 表示影片类型)。 178 | 179 | #### 3.3 评估标准 180 | 181 | 理解了问题和现状,现在我们开始尝试找到解决方案。为此,我们需要想清楚理想中的解决方案,应该满足什么标准: 182 | 183 | - **1、准确性:** 这是核心目标,能够在保证不同场景 / 页面下准确收集埋点数据; 184 | - **2、简洁性:** 使用方法尽可能简单,收敛模板代码; 185 | - **3、可用性:** 尽可能高效稳定,不容易出错,性能开销小。 186 | 187 | #### 3.4 常规解决方案 188 | 189 | **1、逐级传递 —— 通过面向对象的关系逐级传递埋点参数:** 190 | 191 | 通过 Android 框架支持的 Activity / Fragment 参数传递方式和面向对象程序设计,逐级将埋点参数传递到最深层的收藏按钮。例如: 192 | 193 | - **列表页:** Activity -> ViewModel -> FeedFragment (推荐) -> Adapter -> ViewHolder (电影卡片) -> CollectButton (收藏按钮) 194 | 195 | - **详情页:** Activity -> ViewModel -> DetailBottomFragment(底部功能区) -> CollectButton (收藏按钮) 196 | 197 | 缺点 (参数传递困难) :传递数据需要编写大量重复模板代码,工程代码膨胀,增大维护难度。再叠加上组件复用的情况,逐级传递会让代码复杂度非常高,很明显不是一个合理的解决方案。 198 | 199 | **2、Bean 传递 —— 在 Java Bean 中增加字段来收集埋点参数:** 200 | 201 | 缺点 (违背单一职责原则):Java Bean 中侵入了与业务无关的埋点参数,同时会造成 Java Bean 数据冗余,增大维护难度。 202 | 203 | **3、全局单例 —— 通过全局单例对象来收集埋点参数:** 204 | 205 | 这个方案与 “Bean 传递 ” 类似,区别在于埋点参数从 Java Bean 中移动到全局单例中,但缺点还是很明显: 206 | 207 | 缺点 (写入和清理时机):单例会被多个位置写入,一旦被覆盖就无法被恢复,容易导致上报错误;另外清理的时机也难以把握,清理过早会导致埋点参数丢失,清理过晚会污染后面的埋点事件。 208 | 209 | --- 210 | # 4. 西瓜视频方案 211 | 212 | 理解了数据埋点开发中的困难,有没有什么方案可以简化埋点过程中的复杂度呢?我们来讨论下西瓜视频团队分享的一个思路:**基于视图树收集埋点参数。** 213 | 214 | ![](https://github.com/pengxurui/EasyTrack/blob/master/images/EasyTrack%20-%20%E8%A5%BF%E7%93%9C1.png) 215 | 216 | ![](https://github.com/pengxurui/EasyTrack/blob/master/images/EasyTrack%20-%20%E8%A5%BF%E7%93%9C2.png) 217 | 218 | —— 图片引用自西瓜视频技术博客 219 | 220 | 通过分析数据与视图节点的关系可以发现,事件的埋点数据正好分布在视图树的不同节点中。当 “收藏按钮” 触发事件时,只需要沿着视图树逐级向上查找 (通过 View#getParent()) 就可以收集到所有数据。 221 | 222 | 并且,树的分支天然地支持为参数设置不同的值。例如 “推荐 Fragment” 需要上报 “channel : recomment”,而 “电影 Fragment” 需要上报 “channel : film”。因为 Fragment 的根布局对应有视图树中的不同节点,所以在不同 Fragment 中触发的事件最终收集到的 “channel” 参数值也就不同了。Nice~ 223 | 224 | --- 225 | # 5. EasyTrack 埋点框架 226 | 227 | 思路 Get 到了,现在我们来讨论如何应用这个思路来解决问题。贴心的我已经帮你实现为一个框架 EasyTrack。源码地址:https://github.com/pengxurui/EasyTrack 228 | 229 | #### 5.1 添加依赖 230 | 231 | - **1、依赖 JitPack 仓库** 232 | 233 | 在项目级 build.gradle 声明远程仓库: 234 | ``` 235 | allprojects { 236 | repositories { 237 | google() 238 | mavenCentral() 239 | // JitPack 仓库 240 | maven { url "https://jitpack.io" } 241 | } 242 | } 243 | ``` 244 | - **2、依赖 EasyTrack 框架** 245 | 246 | 在模块级 build.gradle 中依赖类库: 247 | ``` 248 | dependencies { 249 | ... 250 | // 依赖 EasyTrack 框架 251 | implementation 'com.github.pengxurui:EasyTrack:v1.0.1' 252 | // 依赖 Kotlin 工具(非必须) 253 | implementation 'com.github.pengxurui:KotlinUtil:1.0.1' 254 | } 255 | ``` 256 | 257 | #### 5.2 依附埋点参数到视图树 258 | 259 | `ITrackModel`接口定义了一个数据填充能力,你可以创建它的实现类来定义一个数据节点,并在 fillTrackParams() 方法中声明参数。例如:MyGoodsViewHolder 实现了 ITrackMode 接口,在 fillTrackParams() 方法中声明参数(goods_id / goods_name)。 260 | 261 | 随后,通过 View 的扩展函数`View.trackModel()`将其依附到视图节点上。扩展函数 View.trackModel() 内部基于 View#setTag() 实现。 262 | 263 | `MyGoodsViewHolder.kt` 264 | ``` 265 | class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ITrackModel { 266 | 267 | private var mItem: GoodsItem? = null 268 | 269 | init { 270 | // Java:EasyTrackUtilsKt.setTrackModel(itemView, this); 271 | itemView.trackModel = this 272 | } 273 | 274 | override fun fillTrackParams(params: TrackParams) { 275 | mItem?.let { 276 | params.setIfNull("goods_id", it.id) 277 | params.setIfNull("goods_name", it.goods_name) 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | `EasyTrackUtils.kt` 284 | ``` 285 | /** 286 | * Attach track model on the view. 287 | */ 288 | var View.trackModel: ITrackModel? 289 | get() = this.getTag(R.id.tag_id_track_model) as? ITrackModel 290 | set(value) { 291 | this.setTag(R.id.tag_id_track_model, value) 292 | } 293 | ``` 294 | 295 | `ITrackModel.kt` 296 | ``` 297 | /** 298 | * 定义数据填充能力 299 | */ 300 | interface ITrackModel : Serializable { 301 | /** 302 | * 数据填充 303 | */ 304 | fun fillTrackParams(params: TrackParams) 305 | } 306 | ``` 307 | 308 | #### 5.3 触发事件埋点 309 | 310 | 在需要埋点的地方,直接通过定义在 View 上的扩展函数 `trackEvent(事件名)`触发埋点事件,它会以该扩展函数的接收者对象为起点,逐级向上层视图节点收集参数。另外,它还有多个定义在 Activity、Fragment、ViewHolder 上的扩展函数,但最终都会调用到 View.trackEvent。 311 | 312 | ``` 313 | class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 314 | fun bind(item: GoodsItem) { 315 | ... 316 | trackEvent(GOODS_EXPOSE) 317 | } 318 | } 319 | ``` 320 | 321 | `EasyTrackUtils.kt` 322 | ``` 323 | @JvmOverloads 324 | fun Activity?.trackEvent(eventName: String, params: TrackParams? = null) = 325 | findRootView(this)?.doTrackEvent(eventName, params) 326 | 327 | @JvmOverloads 328 | fun Fragment?.trackEvent(eventName: String, params: TrackParams? = null) = 329 | this?.requireView()?.doTrackEvent(eventName, params) 330 | 331 | @JvmOverloads 332 | fun RecyclerView.ViewHolder?.trackEvent(eventName: String, params: TrackParams? = null) { 333 | this?.itemView?.let { 334 | if (null == it.parent) { 335 | it.post { it.doTrackEvent(eventName, params) } 336 | } else { 337 | it.doTrackEvent(eventName, params) 338 | } 339 | } 340 | } 341 | 342 | @JvmOverloads 343 | fun View?.trackEvent(eventName: String, params: TrackParams? = null): TrackParams? = 344 | this?.doTrackEvent(eventName, params) 345 | ``` 346 | 347 | 查看 logcat 日志,可以看到以下日志,显示埋点并没有生效。这是因为没有为 EasyTrack 配置埋点数据上报和统计分析的能力。 348 | 349 | `logcat 日志` 350 | ``` 351 | EasyTrackLib: Try track event goods_expose, but the providers is Empty. 352 | ``` 353 | 354 | #### 5.4 实现 ITrackProvider 接口 355 | 356 | EasyTrack 的职责在于收集分散的埋点数据,本身没有提供埋点数据上报和统计分析的能力。因此,你需要实现 ITrackProvider 接口进行依赖注入。例如,这里模拟实现友盟数据埋点提供器,在 onInit() 方法中进行初始化,在 onEvent() 方法中调用友盟 SDK 事件上报方法。 357 | 358 | `MockUmengProvider.kt` 359 | ``` 360 | /** 361 | * 模拟友盟数据上报 362 | */ 363 | class MockUmengProvider : ITrackProvider() { 364 | 365 | companion object { 366 | const val TAG = "Umeng" 367 | } 368 | 369 | /** 370 | * 是否启用 371 | */ 372 | override var enabled = true 373 | 374 | /** 375 | * 名称 376 | */ 377 | override var name = TAG 378 | 379 | /** 380 | * 初始化 381 | */ 382 | override fun onInit() { 383 | Log.d(TAG, "Init Umeng provider.") 384 | } 385 | 386 | /** 387 | * 执行事件上报 388 | */ 389 | override fun onEvent(eventName: String, params: TrackParams) { 390 | Log.d(TAG, params.toString()) 391 | } 392 | } 393 | ``` 394 | 395 | #### 5.5 配置 EasyTrack 396 | 397 | 在应用初始化时,进行 EasyTrack 的初始化配置。我们可以将相关的初始化代码单独封装起来,例如: 398 | 399 | `StatisticsUtils.kt` 400 | ``` 401 | // 模拟友盟数据统计提供器 402 | val umengProvider by lazy { 403 | MockUmengProvider() 404 | } 405 | 406 | // 模拟神策数据统计提供器 407 | val sensorProvider by lazy { 408 | MockSensorProvider() 409 | } 410 | 411 | /** 412 | * 初始化 EasyTrack,在 Application 初始化时调用 413 | */ 414 | fun init(context: Context) { 415 | configStatistics(context) 416 | registerProviders(context) 417 | } 418 | 419 | /** 420 | * 配置 421 | */ 422 | private fun configStatistics(context: Context) { 423 | // 调试开关 424 | EasyTrack.debug = BuildConfig.DEBUG 425 | // 页面间参数映射 426 | EasyTrack.referrerKeyMap = mapOf( 427 | CUR_PAGE to FROM_PAGE, 428 | CUR_TAB to FROM_TAB 429 | ) 430 | } 431 | 432 | /** 433 | * 注册提供器 434 | */ 435 | private fun registerProviders(context: Context) { 436 | EasyTrack.registerProvider(umengProvider) 437 | EasyTrack.registerProvider(sensorProvider) 438 | } 439 | ``` 440 | `EventConstants.java` 441 | ``` 442 | public static final String FROM_PAGE = "from_page"; 443 | public static final String CUR_PAGE = "cur_page"; 444 | public static final String FROM_TAB = "from_tab"; 445 | public static final String CUR_TAB = "cur_tab"; 446 | ``` 447 | 448 | |配置|类型|描述| 449 | |:---|:---|:---| 450 | |debug|Boolean|调试开关| 451 | |referrerKeyMap|Map|全局页面间参数映射| 452 | |registerProvider()|ITrackProvider|底层数据埋点能力| 453 | 454 | 以上步骤是 EasyTrack 的必选步骤,完成后重新执行 trackEvent() 后可以看到以下日志: 455 | 456 | `logcat 日志` 457 | ``` 458 | /EasyTrackLib: 459 | onEvent:goods_expose 460 | goods_id= 10000 461 | goods_name = 商品名 462 | Try track event goods_expose with provider Umeng. 463 | Try track event goods_expose with provider Sensor. 464 | ------------------------------------------------------ 465 | ``` 466 | 467 | #### 5.6 页面间参数映射 468 | 469 | 上一节中有一个`referrerKeyMap`配置项,**定义了全局的页面间参数映射。** 举个例子,在分析不同入口的转化率时,不仅仅需要上报当前页面的数据,还需要上报来源页面的信息。这样我们才能分析用户经过怎样的路径来到当前页面,并最终触发了某个行为。 470 | 471 | 需要注意的是,来源页面的参数往往不能直接添加到当前页面的埋点参数中,这里一般会有一定的转换规则 / 映射关系。例如:**来源页面的 cur_page 参数,在当前页面应该映射为 from_page 参数。** 在这个例子里,我们配置的映射关系是: 472 | 473 | - 来源页面的 cur_page 映射为当前页面的 from_page; 474 | - 来源页面的 cur_tab 映射为当前页面的 from_tab。 475 | 476 | 因此,假设来源页面传递给当前页面的参数是 A,则当前页面在触发事件时的收集参数是 B: 477 | 478 | ``` 479 | A (来源页面): 480 | { 481 | "cur_page" : "list" 482 | ... 483 | } 484 | 485 | B (当前页面): 486 | { 487 | "cur_page" : "detail", 488 | "from_page" : "list", 489 | ... 490 | } 491 | ``` 492 | 493 | `BaseTrackActivity` 实现了页面间参数映射,你可以创建 BaseActivity 类并继承于 BaseTrackActivity,或者将其内部的逻辑迁移到你的 BaseActivity 中。这一步是可选的,如果你不使用页面间参数映射的特性,你那大可不必使用 BaseTrackActivity。 494 | 495 | |操作|描述| 496 | |:---|:---| 497 | |定义映射关系|1、EasyTrack.referrerKeyMap 配置项
2、重写 BaseTrackActivity #referrerKeyMap() 方法| 498 | |传递页面间参数|Intent.referrerSnapshot(TrackParams) 扩展函数| 499 | 500 | `MyGoodsDetailActivity.java` 501 | ``` 502 | public class MyGoodsDetailActivity extends MyBaseActivity { 503 | 504 | private static final String EXTRA_GOODS = "extra_goods"; 505 | 506 | public static void start(Context context, GoodsItem item, TrackParams params) { 507 | Intent intent = new Intent(context, GoodsDetailActivity.class); 508 | intent.putExtra(EXTRA_GOODS, item); 509 | EasyTrackUtilsKt.setReferrerSnapshot(intent, params); 510 | context.startActivity(intent); 511 | } 512 | 513 | @Nullable 514 | @Override 515 | protected String getCurPage() { 516 | return GOODS_DETAIL_NAME; 517 | } 518 | 519 | @Nullable 520 | @Override 521 | public Map referrerKeyMap() { 522 | Map map = new HashMap<>(); 523 | map.put(STORE_ID, STORE_ID); 524 | map.put(STORE_NAME, STORE_NAME); 525 | return map; 526 | } 527 | } 528 | ``` 529 | 530 | 需要注意的是,BaseTrackActivity 不会将来源页面的全部参数都添加到当前页面的参数中,**只有在全局 referrerKeyMap 配置项或 referrerKeyMap() 方法中定义了映射关系的参数,才会添加到当前页面。** 例如:MyGoodsDetailActivity 继承于 BaseActivity,并重写 referrerKeyMap() 定义了感兴趣的参数(STORE_ID、STORE_NAME)。最终触发埋点时的日志如下: 531 | 532 | `logcat 日志` 533 | ``` 534 | /EasyTrackLib: 535 | onEvent:goods_detail_expose 536 | goods_id= 10000 537 | goods_name = 商品名 538 | store_id = 10000 539 | store_name = 商店名 540 | from_page = Recommend 541 | cur_page = goods_detail 542 | Try track event goods_expose with provider Umeng. 543 | Try track event goods_expose with provider Sensor. 544 | ------------------------------------------------------ 545 | ``` 546 | 547 | 在一般的埋点模型中,每个 Activity (页面) 都有对应一个唯一的 page_id,因此你可以重写 fillTrackParams() 方法追加这些固定的参数。例如:MyBaseActivity 定义了 getCurPage() 方法,子类可以通过重写 getCurPage() 来设置 page_id。 548 | 549 | `MyBaseActivity.java` 550 | ``` 551 | abstract class MyBaseActivity : BaseTrackActivity() { 552 | 553 | @CallSuper 554 | override fun fillTrackParams(params: TrackParams) { 555 | super.fillTrackParams(params) 556 | // 填充页面统一参数 557 | getCurPage()?.also { 558 | params.setIfNull(CUR_PAGE, it) 559 | } 560 | } 561 | 562 | protected open fun getCurPage(): String? = null 563 | } 564 | ``` 565 | 566 | #### 5.7 TrackParams 参数容器 567 | 568 | TrackParams 是 EasyTrack 收集参数的中间容器,最终会分发给 ITrackProvider 使用。 569 | 570 | |方法|描述| 571 | |:---|:---| 572 | |set(key: String, value: Any?)|设置参数,无论无何都覆盖| 573 | |setIfNull(key: String, value: Any?)|设置参数,如果已经存在该参数则丢弃| 574 | |get(key: String): String?|获取参数值,参数不存在则返回 null| 575 | |get(key: String, default: String?)|获取参数值,参数不存在则返回默认值 default| 576 | 577 | #### 5.8 使用 Kotlin 委托依附参数 578 | 579 | 如果你觉得每次定义 ITrackModel 数据节点后都需要调用 View.trackModel,你可以使用我定义的 Kotlin 委托 “跳过” 这个步骤,例如: 580 | 581 | `MyFragment.kt` 582 | ``` 583 | private val trackNode by track() 584 | ``` 585 | 586 | `EasyTrackUtils.kt` 587 | ``` 588 | fun F.track(): TrackNodeProperty = FragmentTrackNodeProperty() 589 | 590 | fun RecyclerView.ViewHolder.track(): TrackNodeProperty = 591 | LazyTrackNodeProperty() viewFactory@{ 592 | return@viewFactory itemView 593 | } 594 | 595 | fun View.track(): TrackNodeProperty = LazyTrackNodeProperty() viewFactory@{ 596 | return@viewFactory it 597 | } 598 | ``` 599 | 600 | 如果你还不了解委托属性,可以看下我之前写过的一篇文章,这里不解释其原理了:[Android | ViewBinding 与 Kotlin 委托双剑合璧](https://juejin.cn/post/6958346113552220173) 601 | 602 | --- 603 | # 6. EasyTrack 核心源码 604 | 605 | 这一节,我简单介绍下 EasyTrack 的核心源码,最核心的部分在入口类 EasyTrack 中: 606 | 607 | #### 6.1 doTrackEvent() 608 | 609 | doTrackEvent() 是触发埋点的主方法,主要流程是调用 fillTrackParams() 收集埋点参数,再将参数分发给有效的 ITrackProvider。 610 | 611 | ``` 612 | internal fun Any.doTrackEvent(eventName: String, otherParams: TrackParams? = null): TrackParams? { 613 | 1. 检查是否有有效的 ITrackProvider 614 | 2. 基于视图树递归收集埋点参数(fillTrackParams) 615 | 3. 日志 616 | 4. 将收集到的埋点参数分发给有效的 ITrackProvider 617 | } 618 | ``` 619 | 620 | #### 6.2 fillTrackParams() 621 | 622 | ``` 623 | -> 基于视图树递归收集埋点参数 624 | internal fun fillTrackParams(node: Any?, params: TrackParams? = null): TrackParams { 625 | val result = params ?: TrackParams() 626 | var curNode = node 627 | while (null != curNode) { 628 | when (curNode) { 629 | is View -> { 630 | // 1. 视图节点 631 | if (android.R.id.content == curNode.id) { 632 | // 1.1 Activity 节点 633 | val activity = getActivityFromView(curNode) 634 | if (activity is IPageTrackNode) { 635 | // 1.1.1 IPageTrackNode节点(处理页面间参数映射) 636 | activity.fillTrackParams(result) 637 | curNode = activity.referrerSnapshot() 638 | } else { 639 | // 1.1.2 终止 640 | curNode = null 641 | } 642 | } else { 643 | // 1.2 Activity 视图子节点 644 | curNode.trackModel?.fillTrackParams(result) 645 | curNode = curNode.parent 646 | } 647 | } 648 | is ITrackNode -> { 649 | // 2. 非视图节点 650 | curNode.fillTrackParams(result) 651 | curNode = curNode.parent 652 | } 653 | else -> { 654 | // 3. 终止 655 | curNode = null 656 | } 657 | } 658 | } 659 | return result 660 | } 661 | ``` 662 | 663 | 主要逻辑:从入参 node 为起点,循环获取依附在视图节点上的 ITrackModel 数据节点并调用 fillTrackParams() 方法收集参数,并将循环指针指向 parent。 664 | 665 | --- 666 | # 7. 总结 667 | 668 | EasyTrack 框架的源码我已经放在 Github 上了,源码地址:https://github.com/pengxurui/EasyTrack 我也写了一个简单的 Sample Demo,你可以直接运行体验下。欢迎批评,欢迎 Issue~ 669 | 670 | 说说目前遇到的问题,在处理页面间参数传递时,我们需要依赖 Intent extras 参数。这就导致我们需要在大量创建 Intent 的地方都加入来源页面的埋点参数(注意:即使你不使用 EasyTrack,你也要这么做)。目前我还没有想到比较好的方法,你觉得呢?说说你的看法吧。 671 | 672 | --- 673 | #### 参考资料 674 | - [西瓜客户端埋点实践:基于责任链的埋点框架](https://mp.weixin.qq.com/s/iMn--4FNugtH26G90N1MaQ) —— 何金海(字节)著 675 | - [埋点治理:如何把 App 埋点做到极致?](https://mp.weixin.qq.com/s/O_02RsP9U2N4cXQH5rc0zQ) —— 林乐洋(58)著 676 | - [51 信用卡 Android 自动埋点实践](https://mp.weixin.qq.com/s/P95ATtgT2pgx4bSLCAzi3Q) —— 李传志(51 信用卡)著 677 | - [利用 Live Templates 打造埋点自动化利器](https://juejin.cn/post/6966758717773578270) —— 字节大力智能技术团队 著 678 | - [数据分析从理念到实操](https://sensorsdata.cn/uploads/773b55bdee9e1884963493cda4e73aa1/%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90%E4%BB%8E%E7%90%86%E5%BF%B5%E5%88%B0%E5%AE%9E%E6%93%8D%E7%99%BD%E7%9A%AE%E4%B9%A6.pdf) —— 神策数据 著 679 | 680 | > **创作不易,你的「三连」是丑丑最大的动力,我们下次见!** 681 | 682 | ## Donate 683 | 684 | 如果本仓库对你有帮助,可以请小彭喝杯速溶咖啡。 685 | 686 | ![](https://github.com/pengxurui/AndroidFamily/blob/master/images/%E8%AF%B7%E5%B0%8F%E5%BD%AD%E5%96%9D%E6%9D%AF%E9%80%9F%E6%BA%B6%E5%92%96%E5%95%A1.png) 687 | 688 | ## License 689 | 690 | Copyright [2022] [Peng Xurui] 691 | 692 | Licensed under the Apache License, Version 2.0 (the "License"); 693 | you may not use this file except in compliance with the License. 694 | You may obtain a copy of the License at 695 | 696 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 697 | 698 | Unless required by applicable law or agreed to in writing, software 699 | distributed under the License is distributed on an "AS IS" BASIS, 700 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 701 | See the License for the specific language governing permissions and 702 | limitations under the License. 703 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | apply from: 'dependencies.gradle' 4 | 5 | buildscript { 6 | ext.kotlin_version = "1.4.31" 7 | repositories { 8 | google() 9 | mavenCentral() 10 | } 11 | dependencies { 12 | classpath "com.android.tools.build:gradle:4.2.1" 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | classpath "com.github.dcendents:android-maven-gradle-plugin:1.5" // GitHub Maven 插件 15 | 16 | // NOTE: Do not place your application dependencies here; they belong 17 | // in the individual module build.gradle files 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' } 24 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 25 | maven { url "https://jitpack.io" } 26 | google() 27 | mavenCentral() 28 | } 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } -------------------------------------------------------------------------------- /config.gradle: -------------------------------------------------------------------------------- 1 | project.ext { 2 | 3 | configAppModule = { project -> 4 | project.apply plugin: 'com.android.application' 5 | // kotlin 配置 6 | configKotlin project 7 | // Android 配置 8 | configAndroid(project) 9 | // 通用依赖配置 10 | configDependencies project 11 | // ARouter 配置 12 | // configARouter project 13 | // cpp 配置 14 | // configCpp project 15 | } 16 | 17 | configLibModule = { project -> 18 | project.apply plugin: 'com.android.library' 19 | // kotlin 配置 20 | configKotlin project 21 | // Android 配置 22 | configAndroid(project) 23 | // 通用依赖配置 24 | configDependencies project 25 | // ARouter 配置 26 | // configARouter project 27 | // cpp 配置 28 | // configCpp project 29 | } 30 | 31 | /** 32 | * Android 配置 33 | */ 34 | configAndroid = { project -> 35 | 36 | project.android { 37 | compileSdkVersion APP.compileSdkVersion 38 | buildToolsVersion APP.buildToolsVersion 39 | 40 | defaultConfig { 41 | minSdkVersion APP.minSdkVersion 42 | targetSdkVersion APP.targetSdkVersion 43 | versionCode APP.versionCode 44 | versionName APP.versionName 45 | 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | consumerProguardFiles 'consumer-rules.pro' 48 | 49 | vectorDrawables.useSupportLibrary = true 50 | } 51 | 52 | // maven不缓存,解决SNAPSHOT频繁打包不刷新问题 53 | configurations.all { 54 | resolutionStrategy { 55 | cacheChangingModulesFor 0, 'seconds' 56 | } 57 | } 58 | 59 | buildTypes { 60 | release { 61 | // 大多数module不混淆,少数如需混淆覆盖配置即可 62 | minifyEnabled false 63 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 64 | } 65 | } 66 | 67 | compileOptions { 68 | encoding "UTF8" 69 | sourceCompatibility = 1.8 70 | targetCompatibility = 1.8 71 | } 72 | 73 | kotlinOptions { 74 | jvmTarget = "1.8" 75 | } 76 | 77 | viewBinding { 78 | enabled = true 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Kotlin 配置 85 | */ 86 | configKotlin = { project -> 87 | // 应用 kotlin 插件 88 | project.apply plugin: 'kotlin-android' 89 | project.apply plugin: 'kotlin-kapt' 90 | project.apply plugin: 'kotlin-parcelize' 91 | // Kotlin 运行库 92 | project.dependencies.implementation SDK.ktx_jdk7 93 | // Kotlin 协程库 94 | project.dependencies.implementation SDK.ktx_coroutines 95 | 96 | } 97 | 98 | /** 99 | * cpp配置 100 | */ 101 | configCpp = { project -> 102 | project.android { 103 | defaultConfig { 104 | externalNativeBuild { 105 | cmake { 106 | } 107 | } 108 | } 109 | 110 | externalNativeBuild { 111 | cmake { 112 | path 'src/main/cpp/CMakeLists.txt' 113 | } 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * 依赖配置 120 | */ 121 | configDependencies = { project -> 122 | project.dependencies { 123 | implementation SDK.activityx 124 | // implementation SDK.activityx_ktx 125 | implementation SDK.fragmentx 126 | // implementation SDK.fragmentx_ktx 127 | } 128 | } 129 | 130 | /** 131 | * ARouter 配置 132 | */ 133 | configARouter = { project -> 134 | project.kapt { 135 | arguments { 136 | arg("AROUTER_MODULE_NAME", project.getName()) 137 | } 138 | } 139 | project.dependencies.implementation 'com.alibaba:arouter-api:1.5.2' 140 | project.dependencies.kapt 'com.alibaba:arouter-compiler:1.5.2' 141 | } 142 | } -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | activityxVersion = "1.2.3" 3 | fragmentxVersion = "1.3.3" 4 | kotlinVersion = "1.4.31" 5 | 6 | APP = [ 7 | compileSdkVersion: 30, 8 | buildToolsVersion: "30.0.0", 9 | minSdkVersion : 17, 10 | targetSdkVersion : 28, 11 | versionCode : 1, 12 | versionName : "1.0.0", 13 | ] 14 | 15 | SDK = [ 16 | // JetPack 17 | appcompat : "androidx.appcompat:appcompat:1.2.0", 18 | core_ktx : "androidx.core:core-ktx:1.3.1", 19 | activityx : "androidx.activity:activity:$activityxVersion", 20 | activityx_ktx : "androidx.activity:activity-ktx:$activityxVersion", 21 | fragmentx : "androidx.fragment:fragment:$fragmentxVersion", 22 | fragmentx_ktx : "androidx.fragment:fragment-ktx:$fragmentxVersion", 23 | annotation : "androidx.annotation:annotation:1.0.0", 24 | recyclerview : "androidx.recyclerview:recyclerview:1.1.0", 25 | constraintlayout : "androidx.constraintlayout:constraintlayout:2.0.4", 26 | coordinatorlayout : "androidx.coordinatorlayout:coordinatorlayout:1.1.0", 27 | lifecycle_extensions : "androidx.lifecycle:lifecycle-extensions:2.2.0", 28 | material : "com.google.android.material:material:1.2.1", 29 | 30 | ktx_jdk7 : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion", 31 | ktx_coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9", 32 | sensorsdata : "com.sensorsdata.analytics.android:SensorsAnalyticsSDK:5.2.4", 33 | ] 34 | } -------------------------------------------------------------------------------- /easytrack/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /easytrack/README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/easytrack/README -------------------------------------------------------------------------------- /easytrack/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' // GitHub Maven 插件 3 | apply from: '../config.gradle' 4 | 5 | group = 'com.github.pengxurui' // github 的用户名 6 | 7 | project.ext.configLibModule project 8 | 9 | dependencies { 10 | implementation SDK.ktx_jdk7 11 | implementation SDK.core_ktx 12 | implementation SDK.ktx_coroutines 13 | implementation SDK.appcompat 14 | implementation SDK.activityx 15 | implementation SDK.activityx_ktx 16 | implementation SDK.fragmentx 17 | implementation SDK.fragmentx_ktx 18 | implementation SDK.recyclerview 19 | implementation SDK.lifecycle_extensions 20 | } -------------------------------------------------------------------------------- /easytrack/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/easytrack/consumer-rules.pro -------------------------------------------------------------------------------- /easytrack/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /easytrack/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/EasyTrack.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | import android.util.Log 4 | import android.view.View 5 | import com.pengxr.easytrack.util.getActivityFromView 6 | import com.pengxr.easytrack.util.trackModel 7 | 8 | /** 9 | * Created by pengxr on 2021/8/18. 10 | */ 11 | internal const val TAG = "EasyTrackLib" 12 | 13 | class EasyTrack { 14 | companion object { 15 | 16 | /** 17 | * Underlying statistical provider. 18 | */ 19 | internal var providers: MutableList = ArrayList() 20 | 21 | /** 22 | * Debug or not. 23 | */ 24 | var debug: Boolean = true 25 | 26 | /** 27 | * Global Page referrer key map. For example, [cur_page to from_page] means [cur_page] in last page 28 | * will become [from_page] in current page. 29 | */ 30 | var referrerKeyMap: Map? = null 31 | 32 | /** 33 | * Register underlying statistical provider. 34 | */ 35 | fun registerProvider(provider: ITrackProvider) { 36 | providers.add(provider.apply { 37 | onInit() 38 | }) 39 | } 40 | 41 | /** 42 | * Dispatch event without recursive node tree. 43 | */ 44 | fun dispatchEvent(event: String, params: TrackParams) { 45 | for (provider in providers) { 46 | provider.onEvent(event, params) 47 | } 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * @param eventName Event name. 54 | * @param otherParams Incoming event params 55 | * 56 | * @return Event Params around the node tree. 57 | */ 58 | internal fun Any.doTrackEvent(eventName: String, otherParams: TrackParams? = null): TrackParams? { 59 | // 1. Check whether the underlying statistical provider are available. 60 | if (EasyTrack.providers.isEmpty()) { 61 | Log.d(TAG, "Try track event $eventName, but the providers is Empty.") 62 | return otherParams 63 | } 64 | // 2. Collect data recursively. 65 | val params = if (this is View || this is TrackModel) { 66 | fillTrackParams(this, otherParams) 67 | } else { 68 | otherParams 69 | } 70 | if (null == params) { 71 | return otherParams 72 | } 73 | // 3. Log. 74 | val logStrBuilder = if (EasyTrack.debug) { 75 | StringBuilder().apply { 76 | append(" ") 77 | append("\nonEvent:$eventName") 78 | for ((key, value) in params) { 79 | append("\n$key = $value") 80 | } 81 | } 82 | } else { 83 | null 84 | } 85 | // 4. Do event reporting. 86 | for (provider in EasyTrack.providers) { 87 | if (!provider.enabled) { 88 | logStrBuilder?.append("\nTry track event $eventName, but the provider is disabled.") 89 | continue 90 | } 91 | logStrBuilder?.append("\nTry track event $eventName with provider ${provider.name}.") 92 | provider.onEvent(eventName, params) 93 | } 94 | logStrBuilder?.append("\n------------------------------------------------------")?.also { 95 | Log.d(TAG, it.toString()) 96 | } 97 | return params 98 | } 99 | 100 | /** 101 | * Collect data recursively 102 | */ 103 | internal fun fillTrackParams(node: Any?, params: TrackParams? = null): TrackParams { 104 | val result = params ?: TrackParams() 105 | var curNode = node 106 | while (null != curNode) { 107 | when (curNode) { 108 | is View -> { 109 | if (android.R.id.content == curNode.id) { 110 | // View Root 111 | val activity = getActivityFromView(curNode) 112 | if (activity is IPageTrackNode) { 113 | // Activity node. 114 | activity.fillTrackParams(result) 115 | curNode = activity.referrerSnapshot() 116 | } else { 117 | curNode = null 118 | } 119 | } else { 120 | // View node 121 | curNode.trackModel?.fillTrackParams(result) 122 | curNode = curNode.parent 123 | } 124 | } 125 | is ITrackNode -> { 126 | // Track node 127 | curNode.fillTrackParams(result) 128 | curNode = curNode.parent 129 | } 130 | else -> { 131 | curNode = null 132 | } 133 | } 134 | } 135 | return result 136 | } 137 | 138 | ///** 139 | // * 创建 Event 实例 140 | // */ 141 | //fun View?.newTrackEvent(eventName: String): TrackEvent { 142 | // this?.doTrackEvent(eventName) 143 | //} 144 | 145 | ///** 146 | // * 创建 Event 实例 147 | // */ 148 | //fun ITrackModel?.newTrackEvent(eventName: String): TrackEvent { 149 | // this?.doTrackEvent(eventName) 150 | //} -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/FillStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | import androidx.annotation.IntDef 4 | import com.pengxr.easytrack.core.FillStrategy.Companion.DEFAULT 5 | import com.pengxr.easytrack.core.FillStrategy.Companion.NON_CACHE 6 | 7 | /** 8 | * 数据填充策略 9 | *

10 | * Created by pengxr on 22/8/2021 11 | */ 12 | @IntDef(DEFAULT, NON_CACHE) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class FillStrategy { 15 | companion object { 16 | const val DEFAULT = 0 // 缓存(默认的) 17 | const val NON_CACHE = 1 // 不缓存 18 | } 19 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/IPageTrackNode.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | /** 4 | * Created by pengxr on 10/9/2021 5 | */ 6 | interface IPageTrackNode : ITrackModel { 7 | 8 | fun referrerKeyMap(): Map? 9 | 10 | fun referrerSnapshot(): ITrackNode? 11 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/ITrackModel.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * 定义数据埋点能力 7 | * Created by pengxr on 2021/8/18. 8 | */ 9 | interface ITrackModel : Serializable { 10 | 11 | /** 12 | * 数据填充 13 | */ 14 | fun fillTrackParams(params: TrackParams) 15 | 16 | // /** 17 | // * 启动埋点会话 18 | // */ 19 | // @AnyThread 20 | // fun startSession(sessionName: String): TrackSession { 21 | // return TrackSession().apply { 22 | // sessionMap[sessionName] = this 23 | // } 24 | // } 25 | // 26 | // /** 27 | // * 移除埋点会话 28 | // */ 29 | // @AnyThread 30 | // fun removeSession(sessionName: String) { 31 | // sessionMap[sessionName]?.isEnabled = false 32 | // sessionMap.remove(sessionName) 33 | // } 34 | // 35 | // /** 36 | // * 清除埋点会话 37 | // */ 38 | // @AnyThread 39 | // fun clear() { 40 | // val oldSession = sessionMap 41 | // sessionMap = ConcurrentHashMap() 42 | // 43 | // for ((name, session) in oldSession) { 44 | // session.isEnabled = false 45 | // oldSession.remove(name) 46 | // } 47 | // } 48 | // 49 | // /** 50 | // * 获取埋点会话 51 | // */ 52 | // @AnyThread 53 | // fun getSession(sessionName: String): TrackSession? = sessionMap[sessionName] 54 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/ITrackNode.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | /** 4 | * Created by pengxr on 10/9/2021 5 | */ 6 | interface ITrackNode : ITrackModel { 7 | 8 | val parent: ITrackNode? 9 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/ITrackProvider.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | import androidx.annotation.UiThread 4 | 5 | /** 6 | * Underlying statistical provider. 7 | * 8 | * Created by pengxr on 2021/8/18. 9 | */ 10 | abstract class ITrackProvider { 11 | 12 | /** 13 | * Enable data statistics or not. 14 | */ 15 | abstract var enabled: Boolean 16 | 17 | /** 18 | * The tag of this provider. 19 | */ 20 | abstract var name: String 21 | 22 | /** 23 | * Init the provider. 24 | */ 25 | @UiThread 26 | abstract fun onInit() 27 | 28 | /** 29 | * Do event track. 30 | */ 31 | abstract fun onEvent(eventName: String, params: TrackParams) 32 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/TrackModel.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | import androidx.annotation.CallSuper 4 | import java.io.Serializable 5 | 6 | /** 7 | * 数据节点 8 | * Created by pengxr on 2021/8/18. 9 | */ 10 | class TrackModel : ITrackModel, Serializable { 11 | 12 | protected val params by lazy { 13 | TrackParams() 14 | } 15 | 16 | /** 17 | * 设置参数,会覆盖已有参数 18 | */ 19 | operator fun set(key: String, value: Any?) { 20 | params[key] = value 21 | } 22 | 23 | /** 24 | * 获取参数 25 | */ 26 | operator fun get(key: String) = params[key] 27 | 28 | /** 29 | * 设置参数,不会覆盖已有参数 30 | */ 31 | fun setIfNull(key: String, value: Any?) { 32 | params.setIfNull(key, value) 33 | } 34 | 35 | /** 36 | * 获取参数,为空返回默认值 37 | */ 38 | fun get(key: String, default: String?) = params.get(key, default) 39 | 40 | /** 41 | * 数据填充 42 | */ 43 | @CallSuper 44 | override fun fillTrackParams(params: TrackParams) { 45 | // 合并当前参数 46 | params.merge(this.params) 47 | } 48 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/core/TrackParams.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.core 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * Track Params attach on the node 7 | * Created by pengxr on 2021/8/18. 8 | */ 9 | open class TrackParams : Iterable, Serializable { 10 | 11 | /** 12 | * Internal data. 13 | */ 14 | private val data = HashMap() 15 | 16 | /** 17 | * Set anywhere. 18 | */ 19 | operator fun set(key: String, value: Any?): TrackParams { 20 | data[key] = value?.toString() 21 | return this 22 | } 23 | 24 | /** 25 | * Get by key. 26 | */ 27 | operator fun get(key: String): String? = data[key] 28 | 29 | /** 30 | * Set if null. 31 | */ 32 | fun setIfNull(key: String, value: Any?): TrackParams { 33 | val oldValue = data[key] 34 | if (null == oldValue) { 35 | data[key] = value?.toString() 36 | } 37 | return this 38 | } 39 | 40 | /** 41 | * Get or default. 42 | */ 43 | fun get(key: String, default: String?): String? = data[key] ?: default 44 | 45 | /** 46 | * Merge other object into current object. 47 | */ 48 | fun merge(other: TrackParams?): TrackParams { 49 | if (null != other) { 50 | for ((key, value) in other) { 51 | setIfNull(key, value) 52 | } 53 | } 54 | return this 55 | } 56 | 57 | /** 58 | * Create an iterator. 59 | */ 60 | override fun iterator() = data.iterator() 61 | 62 | override fun toString(): String { 63 | return StringBuilder().apply { 64 | append("[") 65 | for ((key, value) in data) { 66 | append(" $key = $value ,") 67 | } 68 | deleteCharAt(this.length - 1) 69 | append("]") 70 | }.toString() 71 | } 72 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/impl/BaseTrackActivity.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.impl 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.CallSuper 5 | import androidx.annotation.LayoutRes 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.pengxr.easytrack.core.IPageTrackNode 8 | import com.pengxr.easytrack.core.ITrackNode 9 | import com.pengxr.easytrack.core.EasyTrack 10 | import com.pengxr.easytrack.core.TrackParams 11 | import com.pengxr.easytrack.util.getReferrerParams 12 | 13 | /** 14 | * Base Activity with event track,you don't have to used it. 15 | * 16 | * Created by pengxr on 10/9/2021 17 | */ 18 | abstract class BaseTrackActivity : AppCompatActivity, IPageTrackNode { 19 | 20 | constructor () 21 | 22 | constructor (@LayoutRes contentLayoutId: Int) : super(contentLayoutId) 23 | 24 | // The snapshot of referrer page node. 25 | private var referrerSnapshot: ITrackNode? = null 26 | 27 | protected val trackParams by lazy { 28 | TrackParams() 29 | } 30 | 31 | // --------------------------------------------------------------------------------------------- 32 | // Activity 33 | // --------------------------------------------------------------------------------------------- 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | 38 | // Snapshot for referrer page node. 39 | getReferrerSnapshot()?.let { referrerParams -> 40 | referrerSnapshot = object : ITrackNode { 41 | override val parent: ITrackNode? = null 42 | 43 | override fun fillTrackParams(params: TrackParams) { 44 | params.merge(referrerParams) 45 | } 46 | } 47 | } 48 | } 49 | 50 | // --------------------------------------------------------------------------------------------- 51 | // public 52 | // --------------------------------------------------------------------------------------------- 53 | 54 | /** 55 | * Get params from referrer page node. 56 | */ 57 | fun getReferrerSnapshot(): TrackParams? = intent.getReferrerParams()?.let { referrerParams -> 58 | TrackParams().apply { 59 | // Fill referrer params. 60 | fillReferrerKeyMap(referrerKeyMap(), referrerParams, this) 61 | fillReferrerKeyMap(EasyTrack.referrerKeyMap, referrerParams, this) 62 | } 63 | } 64 | 65 | // --------------------------------------------------------------------------------------------- 66 | // IPageTrackNode 67 | // --------------------------------------------------------------------------------------------- 68 | 69 | override fun referrerKeyMap(): Map? = null 70 | 71 | override fun referrerSnapshot(): ITrackNode? = referrerSnapshot 72 | 73 | @CallSuper 74 | override fun fillTrackParams(params: TrackParams) { 75 | params.merge(trackParams) 76 | // You can expose api here, it makes subclasses more convenient to pass parameters. 77 | } 78 | 79 | // --------------------------------------------------------------------------------------------- 80 | // protected 81 | // --------------------------------------------------------------------------------------------- 82 | 83 | private fun fillReferrerKeyMap( 84 | map: Map?, referrerParams: TrackParams, params: TrackParams 85 | ) { 86 | if (map.isNullOrEmpty()) { 87 | return 88 | } 89 | for ((fromKey, toKey) in map) { 90 | val toValue = referrerParams[fromKey] 91 | if (null != toValue) { 92 | params.setIfNull(toKey, toValue) 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/impl/BaseTrackFragment.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.impl 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.annotation.CallSuper 6 | import androidx.annotation.LayoutRes 7 | import androidx.fragment.app.Fragment 8 | import com.pengxr.easytrack.core.ITrackModel 9 | import com.pengxr.easytrack.core.TrackParams 10 | import com.pengxr.easytrack.util.trackModel 11 | 12 | /** 13 | * Base Fragment with event track,You don't have to used it. 14 | *

15 | * Created by pengxr on 10/9/2021 16 | */ 17 | abstract class BaseTrackFragment : Fragment, ITrackModel { 18 | 19 | constructor () 20 | 21 | constructor (@LayoutRes contentLayoutId: Int) : super(contentLayoutId) 22 | 23 | // --------------------------------------------------------------------------------------------- 24 | // Fragment 25 | // --------------------------------------------------------------------------------------------- 26 | 27 | @CallSuper 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | 31 | view.trackModel = this 32 | } 33 | 34 | // --------------------------------------------------------------------------------------------- 35 | // IPageTrackNode 36 | // --------------------------------------------------------------------------------------------- 37 | 38 | override fun fillTrackParams(params: TrackParams) { 39 | } 40 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/session/TrackEvent.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.session 2 | 3 | import com.pengxr.easytrack.core.ITrackModel 4 | import com.pengxr.easytrack.core.TrackParams 5 | 6 | /** 7 | * 埋点事件 8 | *

9 | * Created by pengxr on 21/8/2021 10 | */ 11 | class TrackEvent internal constructor( 12 | val eventName: String, var params: TrackParams? 13 | ) { 14 | 15 | /** 16 | * 声明需要使用埋点会话中的参数 17 | */ 18 | fun with(clazz: Class) { 19 | 20 | } 21 | 22 | /** 23 | * 声明需要使用埋点会话中的参数 24 | */ 25 | fun with(vararg params: Array) { 26 | 27 | } 28 | 29 | /** 30 | * 执行上报 31 | */ 32 | fun emit() { 33 | params = params ?: TrackParams() 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/session/TrackSession.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.session 2 | 3 | import com.pengxr.easytrack.core.TrackParams 4 | 5 | /** 6 | * 埋点会话 7 | *

8 | * Created by pengxr on 21/8/2021 9 | */ 10 | class TrackSession internal constructor() : TrackParams() { 11 | 12 | var isEnabled = true 13 | 14 | // private val map = HashMap, ITrackModel?>() 15 | // 16 | // operator fun set(clazz: Class, model: ITrackModel?) { 17 | // map[clazz] = model 18 | // } 19 | // 20 | // operator fun get(clazz: Class): ITrackModel? = map[clazz] 21 | } -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/util/EasyTrackUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.util 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.util.Log 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.activity.ComponentActivity 11 | import androidx.annotation.IdRes 12 | import androidx.annotation.MainThread 13 | import androidx.core.app.ActivityCompat 14 | import androidx.core.view.ViewCompat 15 | import androidx.fragment.app.DialogFragment 16 | import androidx.fragment.app.Fragment 17 | import androidx.lifecycle.Lifecycle 18 | import androidx.lifecycle.LifecycleObserver 19 | import androidx.lifecycle.LifecycleOwner 20 | import androidx.lifecycle.OnLifecycleEvent 21 | import androidx.recyclerview.widget.RecyclerView 22 | import com.pengxr.easytrack.R 23 | import com.pengxr.easytrack.core.* 24 | import kotlin.properties.ReadOnlyProperty 25 | import kotlin.reflect.KProperty 26 | 27 | /** 28 | * Delegate Property of TrackNode. 29 | * 30 | * Created by pengxr on 26/8/2021 31 | */ 32 | 33 | private const val EXTRA_REFERRER_SNAPSHOT = "referrer_node" 34 | 35 | // ------------------------------------------------------------------------------------------------- 36 | // Java 37 | // ------------------------------------------------------------------------------------------------- 38 | 39 | fun trackNode(view: View): TrackModel { 40 | return TrackModel().apply { 41 | view.trackModel = this 42 | } 43 | } 44 | 45 | fun trackNode(holder: RecyclerView.ViewHolder): TrackModel { 46 | return TrackModel().apply { 47 | holder.itemView.trackModel = this 48 | } 49 | } 50 | 51 | fun trackNode(fragment: Fragment): TrackModel { 52 | return TrackModel().apply { 53 | fragment.requireView().trackModel = this 54 | } 55 | } 56 | 57 | // ------------------------------------------------------------------------------------- 58 | // Kotlin TrackNodeProperty 59 | // ------------------------------------------------------------------------------------- 60 | 61 | fun F.track(): TrackNodeProperty = FragmentTrackNodeProperty() 62 | 63 | fun RecyclerView.ViewHolder.track(): TrackNodeProperty = 64 | LazyTrackNodeProperty() viewFactory@{ 65 | return@viewFactory itemView 66 | } 67 | 68 | fun View.track(): TrackNodeProperty = LazyTrackNodeProperty() viewFactory@{ 69 | return@viewFactory it 70 | } 71 | 72 | // ------------------------------------------------------------------------------------- 73 | // TrackNodeProperty 74 | // ------------------------------------------------------------------------------------- 75 | 76 | private const val TAG = "TrackNodeProperty" 77 | 78 | interface TrackNodeProperty : ReadOnlyProperty { 79 | 80 | /** 81 | * 视图节点 82 | */ 83 | fun getViewNode(thisRef: R): View 84 | 85 | /** 86 | * 清除 87 | */ 88 | @MainThread 89 | fun clear() 90 | } 91 | 92 | class LazyTrackNodeProperty( 93 | private val viewFactory: (R) -> View 94 | ) : TrackNodeProperty { 95 | 96 | private var trackNode: TrackModel? = null 97 | 98 | @Suppress("UNCHECKED_CAST") 99 | @MainThread 100 | override fun getValue(thisRef: R, property: KProperty<*>): TrackModel { 101 | // Already attached 102 | trackNode?.let { return it } 103 | 104 | return TrackModel().also { 105 | this.trackNode = it 106 | } 107 | } 108 | 109 | @MainThread 110 | override fun clear() { 111 | trackNode = null 112 | } 113 | 114 | override fun getViewNode(thisRef: R) = viewFactory(thisRef) 115 | } 116 | 117 | abstract class LifecycleTrackNodeProperty : TrackNodeProperty { 118 | 119 | private var trackNode: TrackModel? = null 120 | 121 | protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner 122 | 123 | @MainThread 124 | override fun getValue(thisRef: R, property: KProperty<*>): TrackModel { 125 | // Already attached 126 | trackNode?.let { return it } 127 | 128 | val lifecycle = getLifecycleOwner(thisRef).lifecycle 129 | val trackNode = TrackModel() 130 | if (lifecycle.currentState == Lifecycle.State.DESTROYED) { 131 | Log.w( 132 | TAG, 133 | "Access to trackNode after Lifecycle is destroyed or hasn't created yet. " + "The instance of trackNode will be not cached." 134 | ) 135 | // We can access to TrackNode after Fragment.onDestroyView(), but don't save it to prevent memory leak 136 | } else { 137 | lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this)) 138 | // attach 139 | this.trackNode = trackNode 140 | getViewNode(thisRef).trackModel = trackNode 141 | } 142 | return trackNode 143 | } 144 | 145 | @MainThread 146 | override fun clear() { 147 | trackNode = null 148 | } 149 | 150 | private class ClearOnDestroyLifecycleObserver( 151 | private val property: LifecycleTrackNodeProperty<*> 152 | ) : LifecycleObserver { 153 | 154 | private companion object { 155 | private val mainHandler = Handler(Looper.getMainLooper()) 156 | } 157 | 158 | @MainThread 159 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) 160 | fun onDestroy(owner: LifecycleOwner) { 161 | mainHandler.post { property.clear() } 162 | } 163 | } 164 | } 165 | 166 | class FragmentTrackNodeProperty : LifecycleTrackNodeProperty() { 167 | 168 | override fun getLifecycleOwner(thisRef: F): LifecycleOwner { 169 | try { 170 | return thisRef.viewLifecycleOwner 171 | } catch (ignored: IllegalStateException) { 172 | error("Fragment doesn't have view associated with it or the view has been destroyed") 173 | } 174 | } 175 | 176 | override fun getViewNode(thisRef: F) = thisRef.requireView() 177 | } 178 | 179 | class DialogFragmentTrackNodeProperty( 180 | ) : LifecycleTrackNodeProperty() { 181 | 182 | override fun getLifecycleOwner(thisRef: F): LifecycleOwner { 183 | return if (thisRef.showsDialog) { 184 | thisRef 185 | } else { 186 | try { 187 | thisRef.viewLifecycleOwner 188 | } catch (ignored: IllegalStateException) { 189 | error( 190 | "Fragment doesn't have view associated with it or the view has been destroyed" 191 | ) 192 | } 193 | } 194 | } 195 | 196 | override fun getViewNode(thisRef: F) = thisRef.requireView() 197 | } 198 | 199 | // ------------------------------------------------------------------------------------- 200 | // Utils 201 | // ------------------------------------------------------------------------------------- 202 | 203 | private fun View.requireViewByIdCompat(@IdRes id: Int): V { 204 | return ViewCompat.requireViewById(this, id) 205 | } 206 | 207 | private fun Activity.requireViewByIdCompat(@IdRes id: Int): V { 208 | return ActivityCompat.requireViewById(this, id) 209 | } 210 | 211 | /** 212 | * Utility to find root view for ViewBinding in Activity 213 | */ 214 | private fun findRootView(activity: Activity?): View? { 215 | val contentView = activity?.findViewById(android.R.id.content) 216 | return when (contentView?.childCount) { 217 | 1 -> contentView.getChildAt(0) 218 | 0 -> null 219 | else -> null 220 | } 221 | } 222 | 223 | private fun DialogFragment.getRootView(viewBindingRootId: Int): View { 224 | val dialog = checkNotNull(dialog) { 225 | "DialogFragment doesn't have dialog. Use viewBinding delegate after onCreateDialog" 226 | } 227 | val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" } 228 | return with(window.decorView) { 229 | if (viewBindingRootId != 0) requireViewByIdCompat( 230 | viewBindingRootId 231 | ) else this 232 | } 233 | } 234 | 235 | /** 236 | * Params from referrer page note. 237 | */ 238 | fun Intent.setReferrerSnapshot(node: ITrackModel?) { 239 | if (null != node) { 240 | setReferrerSnapshot(fillTrackParams(node)) 241 | } 242 | } 243 | 244 | fun Intent.setReferrerSnapshot(node: View?) { 245 | if (null != node) { 246 | setReferrerSnapshot(fillTrackParams(node)) 247 | } 248 | } 249 | 250 | fun Intent.setReferrerSnapshot(params: TrackParams?) { 251 | if (null != params) { 252 | putExtra(EXTRA_REFERRER_SNAPSHOT, params) 253 | } 254 | } 255 | 256 | fun Intent.getReferrerParams(): TrackParams? { 257 | return getSerializableExtra(EXTRA_REFERRER_SNAPSHOT) as TrackParams? 258 | } 259 | 260 | /** 261 | * Attach track model on the view. 262 | */ 263 | var View.trackModel: ITrackModel? 264 | get() = this.getTag(R.id.tag_id_track_model) as? ITrackModel 265 | set(value) { 266 | this.setTag(R.id.tag_id_track_model, value) 267 | } 268 | 269 | /** 270 | * Do event track, it will collect event Params around the node tree. 271 | */ 272 | @JvmOverloads 273 | fun Activity?.trackEvent(eventName: String, params: TrackParams? = null) = 274 | findRootView(this)?.doTrackEvent(eventName, params) 275 | 276 | @JvmOverloads 277 | fun Fragment?.trackEvent(eventName: String, params: TrackParams? = null) = 278 | this?.requireView()?.doTrackEvent(eventName, params) 279 | 280 | @JvmOverloads 281 | fun RecyclerView.ViewHolder?.trackEvent(eventName: String, params: TrackParams? = null) { 282 | this?.itemView?.let { 283 | if (null == it.parent) { 284 | it.post { it.doTrackEvent(eventName, params) } 285 | } else { 286 | it.doTrackEvent(eventName, params) 287 | } 288 | } 289 | } 290 | 291 | @JvmOverloads 292 | fun View?.trackEvent(eventName: String, params: TrackParams? = null): TrackParams? = 293 | this?.doTrackEvent(eventName, params) -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/util/LogUtil.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.util 2 | 3 | /** 4 | * 5 | * 6 | *

7 | * Created by pengxr on 6/9/2021 8 | */ -------------------------------------------------------------------------------- /easytrack/src/main/java/com/pengxr/easytrack/util/ViewUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.easytrack.util 2 | 3 | import android.app.Activity 4 | import android.content.ContextWrapper 5 | import android.view.View 6 | 7 | /** 8 | * Created by pengxr on 10/9/2021 9 | */ 10 | 11 | /** 12 | * try get host activity from view. 13 | * views hosted on floating window like dialog and toast will sure return null. 14 | * 15 | * @return host activity; or null if not available 16 | */ 17 | internal fun getActivityFromView(view: View): Activity? { 18 | var context = view.context 19 | while (context is ContextWrapper) { 20 | if (context is Activity) { 21 | return context 22 | } 23 | context = context.baseContext 24 | } 25 | return null 26 | } 27 | -------------------------------------------------------------------------------- /easytrack/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 14 14:45:01 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/EasyTrack - 困难.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 困难.png -------------------------------------------------------------------------------- /images/EasyTrack - 流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 流程.png -------------------------------------------------------------------------------- /images/EasyTrack - 目录.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 目录.png -------------------------------------------------------------------------------- /images/EasyTrack - 西瓜1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 西瓜1.png -------------------------------------------------------------------------------- /images/EasyTrack - 西瓜2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 西瓜2.png -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: '../config.gradle' 3 | 4 | project.ext.configAppModule project 5 | 6 | dependencies { 7 | implementation SDK.ktx_jdk7 8 | implementation SDK.core_ktx 9 | implementation SDK.ktx_coroutines 10 | implementation SDK.appcompat 11 | implementation SDK.activityx 12 | implementation SDK.activityx_ktx 13 | implementation SDK.fragmentx 14 | implementation SDK.fragmentx_ktx 15 | implementation SDK.recyclerview 16 | implementation SDK.material 17 | implementation SDK.constraintlayout 18 | implementation SDK.coordinatorlayout 19 | implementation SDK.lifecycle_extensions 20 | 21 | implementation project(':easytrack') 22 | implementation 'com.github.pengxurui:KotlinUtil:1.0.1' 23 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.base 2 | 3 | import androidx.annotation.CallSuper 4 | import com.pengxr.easytrack.core.TrackParams 5 | import com.pengxr.easytrack.impl.BaseTrackActivity 6 | import com.pengxr.sample.statistics.EventConstants.CUR_PAGE 7 | 8 | /** 9 | * Created by pengxr on 11/9/2021 10 | */ 11 | abstract class BaseActivity : BaseTrackActivity() { 12 | 13 | // --------------------------------------------------------------------------------------------- 14 | // BaseTrackActivity 15 | // --------------------------------------------------------------------------------------------- 16 | 17 | @CallSuper 18 | override fun fillTrackParams(params: TrackParams) { 19 | super.fillTrackParams(params) 20 | getCurPage()?.also { 21 | params.setIfNull(CUR_PAGE, it) 22 | } 23 | } 24 | 25 | // --------------------------------------------------------------------------------------------- 26 | // protected 27 | // --------------------------------------------------------------------------------------------- 28 | 29 | protected open fun getCurPage(): String? = null 30 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/base/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.base 2 | 3 | import android.app.Application 4 | 5 | /** 6 | * Created by pengxr on 5/9/2021 7 | */ 8 | class MyApplication : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | // Init statistics lib. 14 | init(applicationContext) 15 | } 16 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/entity/GoodsItem.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.entity 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.DrawableRes 5 | import kotlinx.parcelize.Parcelize 6 | 7 | /** 8 | * Created by pengxr on 6/9/2021 9 | */ 10 | @Parcelize 11 | class GoodsItem( 12 | var id: String, 13 | var goods_name: String, 14 | var goods_content: String, 15 | @DrawableRes 16 | var goods_icon: Int 17 | ) : Parcelable { 18 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/entity/StoreDetail.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.entity 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | 7 | /** 8 | * Created by pengxr on 5/9/2021 9 | */ 10 | @Parcelize 11 | class StoreDetail( 12 | var id: String, 13 | var store_name: String, 14 | ) : Parcelable { 15 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/goods/view/GoodsDetailActivity.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.goods.view; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | 8 | import com.pengxr.easytrack.core.TrackParams; 9 | import com.pengxr.easytrack.util.EasyTrackUtilsKt; 10 | import com.pengxr.sample.base.BaseActivity; 11 | import com.pengxr.sample.databinding.GoodsDetailActivityBinding; 12 | import com.pengxr.sample.entity.GoodsItem; 13 | import com.pengxr.sample.goods.vm.GoodsDetailViewModel; 14 | import com.pengxr.sample.utils.ToastUtil; 15 | import com.pengxr.sample.utils.VMCompat; 16 | 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | 20 | import androidx.annotation.Nullable; 21 | 22 | import static com.pengxr.sample.statistics.EventConstants.GOODS_DETAIL_NAME; 23 | import static com.pengxr.sample.statistics.EventConstants.GOODS_ID; 24 | import static com.pengxr.sample.statistics.EventConstants.GOODS_NAME; 25 | import static com.pengxr.sample.statistics.EventConstants.SHARE_CLICK_STEP1; 26 | import static com.pengxr.sample.statistics.EventConstants.STORE_ID; 27 | import static com.pengxr.sample.statistics.EventConstants.STORE_NAME; 28 | 29 | /** 30 | *

31 | * Created by pengxr on 6/9/2021 32 | */ 33 | 34 | public class GoodsDetailActivity extends BaseActivity { 35 | 36 | private static final String EXTRA_GOODS = "extra_goods"; 37 | 38 | private GoodsDetailViewModel mViewModel; 39 | private GoodsDetailActivityBinding binding; 40 | 41 | public static void start(Context context, GoodsItem item, TrackParams params) { 42 | Intent intent = new Intent(context, GoodsDetailActivity.class); 43 | intent.putExtra(EXTRA_GOODS, item); 44 | EasyTrackUtilsKt.setReferrerSnapshot(intent, params); 45 | context.startActivity(intent); 46 | } 47 | 48 | @Nullable 49 | @Override 50 | protected String getCurPage() { 51 | return GOODS_DETAIL_NAME; 52 | } 53 | 54 | @Nullable 55 | @Override 56 | public Map referrerKeyMap() { 57 | Map map = new HashMap<>(); 58 | map.put(STORE_ID, STORE_ID); 59 | map.put(STORE_NAME, STORE_NAME); 60 | return map; 61 | } 62 | 63 | @Override 64 | protected void onCreate(@Nullable Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | 67 | GoodsItem item = getIntent().getParcelableExtra(EXTRA_GOODS); 68 | mViewModel = VMCompat.get(this, GoodsDetailViewModel.class); 69 | mViewModel.init(item); 70 | 71 | binding = GoodsDetailActivityBinding.inflate(getLayoutInflater()); 72 | setContentView(binding.getRoot()); 73 | 74 | init(); 75 | } 76 | 77 | private void init() { 78 | initTrack(); 79 | initView(); 80 | } 81 | 82 | private void initTrack() { 83 | // Add track params. 84 | getTrackParams().set(GOODS_ID, mViewModel.getGoodsItem().getId()); 85 | getTrackParams().set(GOODS_NAME, mViewModel.getGoodsItem().getGoods_name()); 86 | } 87 | 88 | private void initView() { 89 | binding.titleGoodsHome.tvTitle.setText("商品详情"); 90 | binding.titleGoodsHome.ivBack.setVisibility(View.VISIBLE); 91 | binding.titleGoodsHome.ivBack.setOnClickListener(new View.OnClickListener() { 92 | @Override 93 | public void onClick(View view) { 94 | finish(); 95 | } 96 | }); 97 | binding.titleGoodsHome.ivShare.setOnClickListener(new View.OnClickListener() { 98 | @Override 99 | public void onClick(View view) { 100 | ToastUtil.toast(GoodsDetailActivity.this, "分享商品"); 101 | EasyTrackUtilsKt.trackEvent(binding.titleGoodsHome.ivShare, SHARE_CLICK_STEP1); 102 | } 103 | }); 104 | } 105 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/goods/view/GoodsDetailFragment.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.goods.view; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import com.pengxr.easytrack.core.ITrackModel; 9 | import com.pengxr.easytrack.core.TrackParams; 10 | import com.pengxr.easytrack.util.EasyTrackUtilsKt; 11 | import com.pengxr.sample.R; 12 | import com.pengxr.sample.databinding.GoodsDetailFragmentBinding; 13 | import com.pengxr.sample.entity.GoodsItem; 14 | import com.pengxr.sample.goods.vm.GoodsDetailViewModel; 15 | import com.pengxr.sample.statistics.EventConstants; 16 | import com.pengxr.sample.utils.VMCompat; 17 | 18 | import androidx.annotation.NonNull; 19 | import androidx.annotation.Nullable; 20 | import androidx.fragment.app.Fragment; 21 | 22 | import static com.pengxr.sample.statistics.EventConstants.GOODS_DETAIL_CLICK; 23 | import static com.pengxr.sample.statistics.EventConstants.GOODS_DETAIL_IMAGE_NAME; 24 | 25 | /** 26 | * Created by pengxr on 10/9/2021 27 | */ 28 | public class GoodsDetailFragment extends Fragment implements ITrackModel { 29 | 30 | private GoodsDetailViewModel mViewModel; 31 | private GoodsDetailFragmentBinding binding; 32 | 33 | public GoodsDetailFragment() { 34 | super(R.layout.goods_detail_fragment); 35 | } 36 | 37 | @Override 38 | public void fillTrackParams(@NonNull TrackParams params) { 39 | params.set(EventConstants.CUR_PAGE, GOODS_DETAIL_IMAGE_NAME); 40 | } 41 | 42 | @Nullable 43 | @Override 44 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 45 | binding = GoodsDetailFragmentBinding.inflate(inflater); 46 | return binding.getRoot(); 47 | } 48 | 49 | @Override 50 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 51 | super.onViewCreated(view, savedInstanceState); 52 | 53 | EasyTrackUtilsKt.setTrackModel(view, this); 54 | 55 | init(); 56 | } 57 | 58 | private void init() { 59 | mViewModel = VMCompat.get(getActivity(), GoodsDetailViewModel.class); 60 | 61 | initView(); 62 | } 63 | 64 | private void initView() { 65 | GoodsItem item = mViewModel.getGoodsItem(); 66 | binding.ivImage.setImageResource(item.getGoods_icon()); 67 | binding.tvTitle.setText(item.getGoods_name()); 68 | binding.tvContent.setText(item.getGoods_content()); 69 | 70 | binding.ivImage.setOnClickListener(new View.OnClickListener() { 71 | @Override 72 | public void onClick(View view) { 73 | EasyTrackUtilsKt.trackEvent(view, GOODS_DETAIL_CLICK); 74 | } 75 | }); 76 | } 77 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/goods/vm/GoodsDetailViewModel.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.goods.vm; 2 | 3 | import com.pengxr.sample.entity.GoodsItem; 4 | 5 | import androidx.lifecycle.ViewModel; 6 | 7 | /** 8 | * Created by pengxr on 2021/9/15. 9 | */ 10 | public class GoodsDetailViewModel extends ViewModel { 11 | 12 | private GoodsItem mGoodsItem; 13 | 14 | public void init(GoodsItem item) { 15 | this.mGoodsItem = item; 16 | } 17 | 18 | public GoodsItem getGoodsItem() { 19 | return mGoodsItem; 20 | } 21 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/statistics/EventConstants.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.statistics; 2 | 3 | /** 4 | * Created by pengxr on 5/9/2021 5 | */ 6 | public class EventConstants { 7 | 8 | // --------------------------------------------------------------------------------------------- 9 | // Event Name 10 | // --------------------------------------------------------------------------------------------- 11 | 12 | // Click on a goods item. 13 | public static final String GOODS_CLICK = "goods_click"; 14 | // Expose a goods item. 15 | public static final String GOODS_EXPOSE = "goods_expose"; 16 | // Click on a share button. 17 | public static final String SHARE_CLICK_STEP1 = "share_click_step_1"; 18 | // Click on a Share button, and choose a share platform. 19 | public static final String SHARE_CLICK_STEP2 = "share_click_step_2"; 20 | // Click on a goods detail. 21 | public static final String GOODS_DETAIL_CLICK = "goods_detail_click"; 22 | 23 | // --------------------------------------------------------------------------------------------- 24 | // Params Key 25 | // --------------------------------------------------------------------------------------------- 26 | 27 | public static final String FROM_PAGE = "from_page"; 28 | public static final String CUR_PAGE = "cur_page"; 29 | public static final String FROM_TAB = "from_tab"; 30 | public static final String CUR_TAB = "cur_tab"; 31 | public static final String DEVICE_ID = "device_id"; 32 | public static final String LONGITUDE = "longitude"; 33 | public static final String LATITUDE = "latitude"; 34 | public static final String CITY_ID = "city_id"; 35 | public static final String CITY_NAME = "city_name"; 36 | 37 | public static final String STORE_ID = "store_id"; 38 | public static final String STORE_NAME = "store_name"; 39 | public static final String GOODS_ID = "goods_id"; 40 | public static final String GOODS_NAME = "goods_name"; 41 | 42 | // --------------------------------------------------------------------------------------------- 43 | // Page Name. 44 | // --------------------------------------------------------------------------------------------- 45 | 46 | public static final String STORE_HOME_NAME = "store_home"; 47 | public static final String GOODS_DETAIL_NAME = "goods_detail"; 48 | public static final String GOODS_DETAIL_IMAGE_NAME = "goods_detail_image"; 49 | } 50 | -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/statistics/SensorProvider.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.statistics 2 | 3 | import android.util.Log 4 | import com.pengxr.easytrack.core.ITrackProvider 5 | import com.pengxr.easytrack.core.TrackParams 6 | import java.util.* 7 | import kotlin.collections.HashMap 8 | 9 | /** 10 | * Mock Sensor SDK. 11 | *

12 | * Created by pengxr on 5/9/2021 13 | */ 14 | class SensorProvider : ITrackProvider() { 15 | 16 | companion object { 17 | const val TAG = "Sensor" 18 | } 19 | 20 | // Mock internal datas. 21 | private val data = HashMap() 22 | 23 | /** 24 | * Enable data statistics or not. 25 | */ 26 | override var enabled = true 27 | 28 | /** 29 | * The tag of this provider. 30 | */ 31 | override var name = TAG 32 | 33 | /** 34 | * Init the provider. 35 | */ 36 | override fun onInit() { 37 | Log.d(TAG, "Init Sensor provider.") 38 | 39 | registerSuperProperties() 40 | } 41 | 42 | /** 43 | * Do event track. 44 | */ 45 | override fun onEvent(eventName: String, params: TrackParams) { 46 | Log.d(TAG, params.toString()) 47 | } 48 | 49 | private fun registerSuperProperties() { 50 | val params = TrackParams() 51 | params[EventConstants.LONGITUDE] = "113.9" 52 | params[EventConstants.LATITUDE] = 22.5 53 | params[EventConstants.CITY_ID] = "1" 54 | params[EventConstants.CITY_NAME] = "深圳市" 55 | params[EventConstants.DEVICE_ID] = UUID.randomUUID() 56 | for ((key, value) in params) { 57 | data[key] = value 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/statistics/StatisticsUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.base 2 | 3 | import android.content.Context 4 | import com.pengxr.easytrack.core.EasyTrack 5 | import com.pengxr.sample.BuildConfig 6 | import com.pengxr.sample.statistics.EventConstants.* 7 | import com.pengxr.sample.statistics.SensorProvider 8 | import com.pengxr.sample.statistics.UmengProvider 9 | 10 | /** 11 | * Created by pengxr on 5/9/2021 12 | */ 13 | 14 | val umengProvider by lazy { 15 | UmengProvider() 16 | } 17 | 18 | val sensorProvider by lazy { 19 | SensorProvider() 20 | } 21 | 22 | /** 23 | * @param context ApplicationContext 24 | */ 25 | fun init(context: Context) { 26 | configStatistics(context) 27 | registerProviders(context) 28 | } 29 | 30 | private fun configStatistics(context: Context) { 31 | EasyTrack.debug = BuildConfig.DEBUG 32 | EasyTrack.referrerKeyMap = mapOf( 33 | CUR_PAGE to FROM_PAGE, 34 | CUR_TAB to FROM_TAB 35 | ) 36 | } 37 | 38 | private fun registerProviders(context: Context) { 39 | EasyTrack.registerProvider(umengProvider) 40 | EasyTrack.registerProvider(sensorProvider) 41 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/statistics/UmengProvider.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.statistics 2 | 3 | import android.util.Log 4 | import com.pengxr.easytrack.core.ITrackProvider 5 | import com.pengxr.easytrack.core.TrackParams 6 | import java.util.* 7 | import kotlin.collections.HashMap 8 | 9 | /** 10 | * Mock Umeng SDK. 11 | *

12 | * Created by pengxr on 5/9/2021 13 | */ 14 | class UmengProvider : ITrackProvider() { 15 | 16 | companion object { 17 | const val TAG = "Umeng" 18 | } 19 | 20 | // Mock internal datas. 21 | private val data = HashMap() 22 | 23 | /** 24 | * Enable data statistics or not. 25 | */ 26 | override var enabled = true 27 | 28 | /** 29 | * The tag of this provider. 30 | */ 31 | override var name = TAG 32 | 33 | /** 34 | * Init the provider. 35 | */ 36 | override fun onInit() { 37 | Log.d(TAG, "Init Umeng provider.") 38 | 39 | registerSuperProperties() 40 | } 41 | 42 | /** 43 | * Do event track. 44 | */ 45 | override fun onEvent(eventName: String, params: TrackParams) { 46 | Log.d(TAG, params.toString()) 47 | } 48 | 49 | private fun registerSuperProperties() { 50 | val params = TrackParams() 51 | params[EventConstants.LONGITUDE] = "113.9" 52 | params[EventConstants.LATITUDE] = 22.5 53 | params[EventConstants.CITY_ID] = "1" 54 | params[EventConstants.CITY_NAME] = "深圳市" 55 | params[EventConstants.DEVICE_ID] = UUID.randomUUID() 56 | for ((key, value) in params) { 57 | data[key] = value 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/store/view/StoreHomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.store.view 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.FragmentPagerAdapter 6 | import com.pengxr.easytrack.util.trackEvent 7 | import com.pengxr.ktx.delegate.viewBinding 8 | import com.pengxr.sample.R 9 | import com.pengxr.sample.base.BaseActivity 10 | import com.pengxr.sample.databinding.StoreHomeActivityBinding 11 | import com.pengxr.sample.statistics.EventConstants.* 12 | import com.pengxr.sample.store.vm.StoreHomeViewModel 13 | import com.pengxr.sample.utils.ToastUtil 14 | import com.pengxr.sample.utils.VMCompat 15 | 16 | /** 17 | * Created by pengxr on 5/9/2021 18 | */ 19 | class StoreHomeActivity : BaseActivity() { 20 | 21 | private val viewModel by lazy { 22 | VMCompat.get(this, StoreHomeViewModel::class.java) 23 | } 24 | 25 | private val binding by viewBinding(StoreHomeActivityBinding::bind) 26 | 27 | override fun getCurPage() = STORE_HOME_NAME 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | setContentView(R.layout.store_home_activity) 32 | 33 | init() 34 | } 35 | 36 | private fun init() { 37 | initView() 38 | initObserve() 39 | 40 | fetchData() 41 | } 42 | 43 | private fun initView() { 44 | with(binding) { 45 | titleStoreHome.ivBack.visibility = View.INVISIBLE 46 | titleStoreHome.ivShare.setOnClickListener { ivShare -> 47 | ToastUtil.toast(this@StoreHomeActivity, "分享商店") 48 | ivShare.trackEvent(SHARE_CLICK_STEP1) 49 | } 50 | val titles = arrayOf("推荐", "最新") 51 | val fragments = arrayOf(StoreRecommendFragment(), StoreNewestFragment()) 52 | 53 | for (title in titles) { 54 | tabStoreHome.addTab(tabStoreHome.newTab().setText(title)) 55 | } 56 | pagerStoreHome.adapter = object : FragmentPagerAdapter(supportFragmentManager) { 57 | override fun getCount() = fragments.size 58 | 59 | override fun getItem(position: Int) = fragments[position] 60 | 61 | override fun getPageTitle(position: Int) = titles[position] 62 | } 63 | tabStoreHome.setupWithViewPager(pagerStoreHome, false) 64 | } 65 | } 66 | 67 | private fun initObserve() { 68 | viewModel.storeDetailLiveData.observe(this) { detail -> 69 | // Add track params. 70 | trackParams[STORE_ID] = detail.id 71 | trackParams[STORE_NAME] = detail.store_name 72 | binding.titleStoreHome.tvTitle.text = detail.store_name 73 | } 74 | } 75 | 76 | private fun fetchData() { 77 | viewModel.fetchData(this) 78 | } 79 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/store/view/StoreNewestFragment.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.store.view 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.fragment.app.Fragment 7 | import androidx.recyclerview.widget.RecyclerView 8 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 9 | import com.pengxr.easytrack.util.track 10 | import com.pengxr.ktx.delegate.viewBinding 11 | import com.pengxr.sample.R 12 | import com.pengxr.sample.databinding.LayoutFragmentBinding 13 | import com.pengxr.sample.entity.GoodsItem 14 | import com.pengxr.sample.statistics.EventConstants.CUR_PAGE 15 | import com.pengxr.sample.store.vm.StoreHomeViewModel 16 | import com.pengxr.sample.store.widget.GoodsViewHolder 17 | import com.pengxr.sample.store.widget.inflater 18 | import com.pengxr.sample.utils.VMCompat 19 | import com.pengxr.sample.widget.SpacingDecoration 20 | 21 | /** 22 | * Created by pengxr on 11/9/2021 23 | */ 24 | class StoreNewestFragment : Fragment(R.layout.layout_fragment) { 25 | 26 | private val viewModel by lazy { 27 | VMCompat.get(requireActivity(), StoreHomeViewModel::class.java) 28 | } 29 | 30 | private val binding by viewBinding(LayoutFragmentBinding::bind) 31 | 32 | private val trackNode by track() 33 | 34 | private val adapter = GoodsAdapter() 35 | 36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 37 | super.onViewCreated(view, savedInstanceState) 38 | 39 | init() 40 | } 41 | 42 | private fun init() { 43 | initView() 44 | initTrack() 45 | } 46 | 47 | private fun initView() { 48 | with(binding) { 49 | rv.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) 50 | rv.addItemDecoration(SpacingDecoration(requireContext(), 8, 8).apply { 51 | setOutSpacing(requireContext(), 8, 8, 8, 8) 52 | }) 53 | rv.adapter = adapter 54 | } 55 | 56 | viewModel.newestGoodsList.observe(viewLifecycleOwner) { list -> 57 | adapter.setData(list) 58 | } 59 | viewModel.fetchNewestGoodsList(requireContext()) 60 | } 61 | 62 | private fun initTrack() { 63 | trackNode[CUR_PAGE] = "Newest" 64 | } 65 | 66 | private class GoodsAdapter : RecyclerView.Adapter() { 67 | 68 | private val data = ArrayList() 69 | 70 | fun setData(data: List?) { 71 | this.data.clear() 72 | data?.let { 73 | this.data.addAll(data) 74 | } 75 | notifyDataSetChanged() 76 | } 77 | 78 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 79 | inflater(parent.context, parent) 80 | 81 | override fun onBindViewHolder(holder: GoodsViewHolder, position: Int) = 82 | holder.bind(data[position]) 83 | 84 | override fun getItemCount() = data.size 85 | } 86 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/store/view/StoreRecommendFragment.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.store.view 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.fragment.app.Fragment 7 | import androidx.recyclerview.widget.RecyclerView 8 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 9 | import androidx.recyclerview.widget.StaggeredGridLayoutManager.VERTICAL 10 | import com.pengxr.easytrack.util.track 11 | import com.pengxr.ktx.delegate.viewBinding 12 | import com.pengxr.sample.R 13 | import com.pengxr.sample.databinding.LayoutFragmentBinding 14 | import com.pengxr.sample.entity.GoodsItem 15 | import com.pengxr.sample.statistics.EventConstants.CUR_PAGE 16 | import com.pengxr.sample.store.vm.StoreHomeViewModel 17 | import com.pengxr.sample.store.widget.GoodsViewHolder 18 | import com.pengxr.sample.store.widget.inflater 19 | import com.pengxr.sample.utils.VMCompat 20 | import com.pengxr.sample.widget.SpacingDecoration 21 | 22 | /** 23 | * Created by pengxr on 5/9/2021 24 | */ 25 | class StoreRecommendFragment : Fragment(R.layout.layout_fragment) { 26 | 27 | private val viewModel by lazy { 28 | VMCompat.get(requireActivity(), StoreHomeViewModel::class.java) 29 | } 30 | 31 | private val binding by viewBinding(LayoutFragmentBinding::bind) 32 | 33 | private val trackNode by track() 34 | 35 | private val adapter = GoodsAdapter() 36 | 37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 38 | super.onViewCreated(view, savedInstanceState) 39 | 40 | init() 41 | } 42 | 43 | private fun init() { 44 | initView() 45 | initTrack() 46 | } 47 | 48 | private fun initView() { 49 | with(binding) { 50 | rv.layoutManager = StaggeredGridLayoutManager(2, VERTICAL) 51 | rv.addItemDecoration(SpacingDecoration(requireContext(), 8, 8).apply { 52 | setOutSpacing(requireContext(), 8, 8, 8, 8) 53 | }) 54 | rv.adapter = adapter 55 | } 56 | 57 | viewModel.recommendGoodsList.observe(viewLifecycleOwner) { list -> 58 | adapter.setData(list) 59 | } 60 | viewModel.fetchRecommendGoodsList(requireContext()) 61 | } 62 | 63 | private fun initTrack() { 64 | trackNode[CUR_PAGE] = "Recommend" 65 | } 66 | 67 | private class GoodsAdapter : RecyclerView.Adapter() { 68 | 69 | private val data = ArrayList() 70 | 71 | fun setData(data: List?) { 72 | this.data.clear() 73 | data?.let { 74 | this.data.addAll(data) 75 | } 76 | notifyDataSetChanged() 77 | } 78 | 79 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 80 | inflater(parent.context, parent) 81 | 82 | override fun onBindViewHolder(holder: GoodsViewHolder, position: Int) = 83 | holder.bind(data[position]) 84 | 85 | override fun getItemCount() = data.size 86 | } 87 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/store/vm/StoreHomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.store.vm 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.pengxr.sample.R 7 | import com.pengxr.sample.entity.GoodsItem 8 | import com.pengxr.sample.entity.StoreDetail 9 | 10 | /** 11 | * Created by pengxr on 5/9/2021 12 | */ 13 | class StoreHomeViewModel : ViewModel() { 14 | 15 | val storeDetailLiveData = MutableLiveData() 16 | 17 | val recommendGoodsList = MutableLiveData>() 18 | 19 | val newestGoodsList = MutableLiveData>() 20 | 21 | val storeDetail: StoreDetail? 22 | get() = storeDetailLiveData.value 23 | 24 | fun fetchData(context: Context) { 25 | storeDetailLiveData.value = StoreDetail("10000", "商店") 26 | } 27 | 28 | fun fetchRecommendGoodsList(context: Context) { 29 | recommendGoodsList.value = listOf( 30 | GoodsItem("1000", "自行车", "没有轮子的自行车", R.drawable.icon_bike), 31 | GoodsItem("1001", "冰淇淋", "已融化的冰淇淋", R.drawable.icon_icecream), 32 | GoodsItem("1002", "吉他", "断了弦的吉他", R.drawable.icon_guita), 33 | GoodsItem("1003", "包子", "吃剩下的包子", R.drawable.icon_baozi), 34 | GoodsItem("1004", "猫", "会咬人的猫", R.drawable.icon_cat), 35 | GoodsItem("1005", "面条", "隔夜的面条", R.drawable.icon_noodle) 36 | ) 37 | } 38 | 39 | fun fetchNewestGoodsList(context: Context) { 40 | newestGoodsList.value = listOf( 41 | GoodsItem("1000", "自行车", "没有轮子的自行车", R.drawable.icon_bike), 42 | GoodsItem("1001", "冰淇淋", "已融化的冰淇淋", R.drawable.icon_icecream), 43 | GoodsItem("1002", "吉他", "断了弦的吉他", R.drawable.icon_guita), 44 | GoodsItem("1003", "包子", "吃剩下的包子", R.drawable.icon_baozi), 45 | GoodsItem("1004", "猫", "会咬人的猫", R.drawable.icon_cat), 46 | GoodsItem("1005", "面条", "隔夜的面条", R.drawable.icon_noodle) 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/store/widget/GoodsViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.store.widget 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Outline 6 | import android.os.Build 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.view.ViewOutlineProvider 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.pengxr.easytrack.util.track 13 | import com.pengxr.easytrack.util.trackEvent 14 | import com.pengxr.ktx.delegate.viewBinding 15 | import com.pengxr.sample.R 16 | import com.pengxr.sample.databinding.StoreHomeGoodsItemBinding 17 | import com.pengxr.sample.entity.GoodsItem 18 | import com.pengxr.sample.goods.view.GoodsDetailActivity 19 | import com.pengxr.sample.statistics.EventConstants.* 20 | import com.pengxr.sample.utils.DensityUtil 21 | 22 | /** 23 | * Created by pengxr on 6/9/2021 24 | */ 25 | 26 | fun inflater(context: Context, parent: ViewGroup) = GoodsViewHolder( 27 | LayoutInflater.from(context).inflate(R.layout.store_home_goods_item, parent, false) 28 | ) 29 | 30 | class GoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 31 | 32 | private var mItem: GoodsItem? = null 33 | private val binding by viewBinding(StoreHomeGoodsItemBinding::bind) 34 | 35 | private val trackNode by track() 36 | 37 | init { 38 | itemView.setOnClickListener { 39 | mItem?.let { item -> 40 | itemView.trackEvent(GOODS_CLICK)?.also { 41 | GoodsDetailActivity.start(itemView.context, item, it) 42 | } 43 | } 44 | } 45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 46 | itemView.outlineProvider = object : ViewOutlineProvider() { 47 | @SuppressLint("NewApi") 48 | override fun getOutline(view: View, outline: Outline) { 49 | val d: Int = DensityUtil.dip2px(view.context, 6F) 50 | outline.setRoundRect(0, 0, view.measuredWidth, view.measuredHeight, d.toFloat()) 51 | } 52 | } 53 | itemView.clipToOutline = true 54 | } 55 | } 56 | 57 | fun bind(item: GoodsItem) { 58 | mItem = item 59 | with(binding) { 60 | tvTitle.text = item.goods_name 61 | tvContent.text = item.goods_content 62 | ivImage.setImageResource(item.goods_icon) 63 | 64 | trackNode[GOODS_ID] = item.id 65 | trackNode[GOODS_NAME] = item.goods_name 66 | 67 | trackEvent(GOODS_EXPOSE) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/utils/DensityUtil.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.utils; 2 | 3 | import android.content.Context; 4 | 5 | public class DensityUtil { 6 | 7 | public static int dip2px(Context context, float dpValue) { 8 | float scale = context.getResources().getDisplayMetrics().density; 9 | return (int) (dpValue * scale + 0.5f); 10 | } 11 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/utils/ToastUtil.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.utils; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | /** 7 | * Created by pengxr on 5/9/2021 8 | */ 9 | public class ToastUtil { 10 | 11 | public static void toast(Context context, String str) { 12 | Toast.makeText(context, str, Toast.LENGTH_SHORT).show(); 13 | } 14 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/utils/VMCompat.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.utils; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.fragment.app.Fragment; 5 | import androidx.fragment.app.FragmentActivity; 6 | import androidx.lifecycle.ViewModel; 7 | import androidx.lifecycle.ViewModelProvider; 8 | import androidx.lifecycle.ViewModelProviders; 9 | import androidx.lifecycle.ViewModelStoreOwner; 10 | 11 | public class VMCompat { 12 | 13 | public static T get(@NonNull ViewModelStoreOwner owner, 14 | @NonNull Class modelClass) { 15 | return new ViewModelProvider(owner).get(modelClass); 16 | } 17 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/widget/CircleImageView.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapShader; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Matrix; 10 | import android.graphics.Paint; 11 | import android.graphics.Rect; 12 | import android.graphics.RectF; 13 | import android.graphics.Shader; 14 | import android.graphics.drawable.BitmapDrawable; 15 | import android.graphics.drawable.ColorDrawable; 16 | import android.graphics.drawable.Drawable; 17 | import android.net.Uri; 18 | import android.util.AttributeSet; 19 | 20 | import androidx.appcompat.widget.AppCompatImageView; 21 | 22 | public class CircleImageView extends AppCompatImageView { 23 | 24 | // private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; 25 | 26 | private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; 27 | private static final int COLORDRAWABLE_DIMENSION = 1; 28 | 29 | private static final int DEFAULT_BORDER_WIDTH = 0; 30 | private static final int DEFAULT_BORDER_COLOR = Color.BLACK; 31 | 32 | private final RectF mDrawableRect = new RectF(); 33 | private final RectF mBorderRect = new RectF(); 34 | 35 | private final Matrix mShaderMatrix = new Matrix(); 36 | private final Paint mBitmapPaint = new Paint(); 37 | private final Paint mBorderPaint = new Paint(); 38 | 39 | private int mBorderColor = DEFAULT_BORDER_COLOR; 40 | private int mBorderWidth = DEFAULT_BORDER_WIDTH; 41 | private int mCornerWidth = DEFAULT_BORDER_WIDTH; 42 | 43 | private Bitmap mBitmap; 44 | private BitmapShader mBitmapShader; 45 | private int mBitmapWidth; 46 | private int mBitmapHeight; 47 | 48 | private float mDrawableRadius; 49 | private float mBorderRadius; 50 | 51 | private boolean mReady; 52 | private boolean mSetupPending; 53 | 54 | public CircleImageView(Context context) { 55 | super(context); 56 | 57 | init(); 58 | } 59 | 60 | public CircleImageView(Context context, AttributeSet attrs) { 61 | this(context, attrs, 0); 62 | } 63 | 64 | public CircleImageView(Context context, AttributeSet attrs, int defStyle) { 65 | super(context, attrs, defStyle); 66 | 67 | TypedArray a = context.obtainStyledAttributes(attrs, com.pengxr.sample.R.styleable.CircleImageView, defStyle, 0); 68 | 69 | mBorderWidth = a.getDimensionPixelSize(com.pengxr.sample.R.styleable.CircleImageView_border_width, DEFAULT_BORDER_WIDTH); 70 | mBorderColor = a.getColor(com.pengxr.sample.R.styleable.CircleImageView_border_color, DEFAULT_BORDER_COLOR); 71 | mCornerWidth = a.getDimensionPixelSize(com.pengxr.sample.R.styleable.CircleImageView_corner_width, DEFAULT_BORDER_WIDTH); 72 | 73 | a.recycle(); 74 | 75 | init(); 76 | } 77 | 78 | private void init() { 79 | // super.setScaleType(SCALE_TYPE); 80 | mReady = true; 81 | 82 | if (mSetupPending) { 83 | setup(); 84 | mSetupPending = false; 85 | } 86 | } 87 | 88 | // @Override 89 | // public ScaleType getScaleType() { 90 | // return SCALE_TYPE; 91 | // } 92 | // 93 | // @Override 94 | // public void setScaleType(ScaleType scaleType) { 95 | // if (scaleType != SCALE_TYPE) { 96 | // throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)); 97 | // } 98 | // } 99 | 100 | @Override 101 | protected void onDraw(Canvas canvas) { 102 | if (getDrawable() == null) { 103 | return; 104 | } 105 | if (mCornerWidth == 0) { 106 | canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint); 107 | if (mBorderWidth != 0) { 108 | canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint); 109 | } 110 | } else { 111 | canvas.drawRoundRect(new RectF(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()), 112 | mCornerWidth, mCornerWidth, mBitmapPaint); 113 | } 114 | } 115 | 116 | @Override 117 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 118 | super.onSizeChanged(w, h, oldw, oldh); 119 | setup(); 120 | } 121 | 122 | public int getBorderColor() { 123 | return mBorderColor; 124 | } 125 | 126 | public void setBorderColor(int borderColor) { 127 | if (borderColor == mBorderColor) { 128 | return; 129 | } 130 | 131 | mBorderColor = borderColor; 132 | mBorderPaint.setColor(mBorderColor); 133 | invalidate(); 134 | } 135 | 136 | public int getBorderWidth() { 137 | return mBorderWidth; 138 | } 139 | 140 | public void setBorderWidth(int borderWidth) { 141 | if (borderWidth == mBorderWidth) { 142 | return; 143 | } 144 | 145 | mBorderWidth = borderWidth; 146 | setup(); 147 | } 148 | 149 | @Override 150 | public void setImageBitmap(Bitmap bm) { 151 | super.setImageBitmap(bm); 152 | mBitmap = bm; 153 | setup(); 154 | } 155 | 156 | @Override 157 | public void setImageDrawable(Drawable drawable) { 158 | super.setImageDrawable(drawable); 159 | mBitmap = getBitmapFromDrawable(drawable); 160 | setup(); 161 | } 162 | 163 | @Override 164 | public void setImageResource(int resId) { 165 | super.setImageResource(resId); 166 | mBitmap = getBitmapFromDrawable(getDrawable()); 167 | setup(); 168 | } 169 | 170 | @Override 171 | public void setImageURI(Uri uri) { 172 | super.setImageURI(uri); 173 | mBitmap = getBitmapFromDrawable(getDrawable()); 174 | setup(); 175 | } 176 | 177 | private Bitmap getBitmapFromDrawable(Drawable drawable) { 178 | if (drawable == null) { 179 | return null; 180 | } 181 | 182 | if (drawable instanceof BitmapDrawable) { 183 | return ((BitmapDrawable) drawable).getBitmap(); 184 | } 185 | 186 | try { 187 | Bitmap bitmap; 188 | 189 | if (drawable instanceof ColorDrawable) { 190 | bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG); 191 | } else { 192 | int intrinsicWidth = drawable.getIntrinsicWidth(); 193 | int intrinsicHeight = drawable.getIntrinsicHeight(); 194 | 195 | if (intrinsicWidth <= 0 || intrinsicHeight <= 0) { 196 | Rect bounds = drawable.getBounds(); 197 | intrinsicWidth = bounds.width(); 198 | intrinsicHeight = bounds.height(); 199 | } 200 | 201 | if (intrinsicWidth <= 0 || intrinsicHeight <= 0) { 202 | intrinsicWidth = intrinsicHeight = COLORDRAWABLE_DIMENSION; 203 | } 204 | 205 | bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, BITMAP_CONFIG); 206 | } 207 | 208 | Canvas canvas = new Canvas(bitmap); 209 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 210 | drawable.draw(canvas); 211 | return bitmap; 212 | } catch (OutOfMemoryError e) { 213 | return null; 214 | } 215 | } 216 | 217 | private void setup() { 218 | if (!mReady) { 219 | mSetupPending = true; 220 | return; 221 | } 222 | 223 | if (mBitmap == null) { 224 | return; 225 | } 226 | 227 | mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 228 | 229 | mBitmapPaint.setAntiAlias(true); 230 | mBitmapPaint.setShader(mBitmapShader); 231 | 232 | mBorderPaint.setStyle(Paint.Style.STROKE); 233 | mBorderPaint.setAntiAlias(true); 234 | mBorderPaint.setColor(mBorderColor); 235 | mBorderPaint.setStrokeWidth(mBorderWidth); 236 | 237 | mBitmapHeight = mBitmap.getHeight(); 238 | mBitmapWidth = mBitmap.getWidth(); 239 | 240 | mBorderRect.set(0, 0, getWidth(), getHeight()); 241 | mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2, (mBorderRect.width() - mBorderWidth) / 2); 242 | 243 | mDrawableRect.set(mBorderWidth, mBorderWidth, mBorderRect.width() - mBorderWidth, mBorderRect.height() - mBorderWidth); 244 | mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2); 245 | 246 | updateShaderMatrix(); 247 | invalidate(); 248 | } 249 | 250 | private void updateShaderMatrix() { 251 | float scale; 252 | float dx = 0; 253 | float dy = 0; 254 | 255 | mShaderMatrix.set(null); 256 | 257 | if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { 258 | scale = mDrawableRect.height() / (float) mBitmapHeight; 259 | dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; 260 | } else { 261 | scale = mDrawableRect.width() / (float) mBitmapWidth; 262 | dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; 263 | } 264 | 265 | mShaderMatrix.setScale(scale, scale); 266 | mShaderMatrix.postTranslate((int) (dx + 0.5f) + mBorderWidth, (int) (dy + 0.5f) + mBorderWidth); 267 | 268 | mBitmapShader.setLocalMatrix(mShaderMatrix); 269 | } 270 | 271 | } 272 | -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/widget/HackyViewPager.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.widget; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | 7 | import androidx.viewpager.widget.ViewPager; 8 | 9 | /** 10 | * Hacky fix for Issue #4 and 11 | * http://code.google.com/p/android/issues/detail?id=18990 12 | * 13 | * ScaleGestureDetector seems to mess up the touch events, which means that 14 | * ViewGroups which make use of onInterceptTouchEvent throw a lot of 15 | * IllegalArgumentException: pointerIndex out of range. 16 | * 17 | * There's not much I can do in my code for now, but we can mask the result by 18 | * just catching the problem and ignoring it. 19 | * 20 | * @author Chris Banes 21 | */ 22 | public class HackyViewPager extends ViewPager { 23 | 24 | public HackyViewPager(Context context) { 25 | super(context); 26 | } 27 | 28 | public HackyViewPager(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | } 31 | 32 | @Override 33 | public boolean onInterceptTouchEvent(MotionEvent ev) { 34 | try { 35 | return super.onInterceptTouchEvent(ev); 36 | } catch (IllegalArgumentException e) { 37 | e.printStackTrace(); 38 | return false; 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/widget/HeightAutoFitSquareImageView.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.widget; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import androidx.appcompat.widget.AppCompatImageView; 7 | 8 | /** 9 | * Square picture with Height adaptive. 10 | */ 11 | public class HeightAutoFitSquareImageView extends AppCompatImageView { 12 | 13 | public HeightAutoFitSquareImageView(Context context) { 14 | super(context); 15 | } 16 | 17 | public HeightAutoFitSquareImageView(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public HeightAutoFitSquareImageView(Context context, AttributeSet attrs, int defStyleAttr) { 22 | super(context, attrs, defStyleAttr); 23 | } 24 | 25 | @Override 26 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 27 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 28 | int measuredW = getMeasuredWidth(); 29 | setMeasuredDimension(measuredW, measuredW); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/widget/ImmersiveStatusBarSpace.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.widget; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.util.AttributeSet; 6 | import android.view.View; 7 | 8 | import java.lang.reflect.Field; 9 | 10 | public class ImmersiveStatusBarSpace extends View { 11 | 12 | private int height; 13 | 14 | public ImmersiveStatusBarSpace(Context context) { 15 | this(context, (AttributeSet) null); 16 | } 17 | 18 | public ImmersiveStatusBarSpace(Context context, AttributeSet attrs) { 19 | this(context, attrs, 0); 20 | } 21 | 22 | public ImmersiveStatusBarSpace(Context context, AttributeSet attrs, int defStyleAttr) { 23 | super(context, attrs, defStyleAttr); 24 | this.height = Build.VERSION.SDK_INT >= 19 ? getStatusBarHeight(this.getContext()) : 0; 25 | } 26 | 27 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 28 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 29 | this.setMeasuredDimension(this.getMeasuredWidth(), this.height); 30 | } 31 | 32 | public static int getStatusBarHeight(Context context) { 33 | Class c = null; 34 | Object obj = null; 35 | Field field = null; 36 | int x = 0, sbar = 0; 37 | try { 38 | c = Class.forName("com.android.internal.R$dimen"); 39 | obj = c.newInstance(); 40 | field = c.getField("status_bar_height"); 41 | x = Integer.parseInt(field.get(obj).toString()); 42 | sbar = context.getResources().getDimensionPixelSize(x); 43 | } catch (Exception E) { 44 | E.printStackTrace(); 45 | } 46 | return sbar; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sample/src/main/java/com/pengxr/sample/widget/SpacingDecoration.java: -------------------------------------------------------------------------------- 1 | package com.pengxr.sample.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.graphics.Paint; 6 | import android.graphics.Rect; 7 | import android.view.View; 8 | 9 | import com.pengxr.sample.utils.DensityUtil; 10 | 11 | import androidx.annotation.ColorInt; 12 | import androidx.annotation.Dimension; 13 | import androidx.annotation.NonNull; 14 | import androidx.recyclerview.widget.GridLayoutManager; 15 | import androidx.recyclerview.widget.LinearLayoutManager; 16 | import androidx.recyclerview.widget.RecyclerView; 17 | import androidx.recyclerview.widget.StaggeredGridLayoutManager; 18 | 19 | import static androidx.annotation.Dimension.DP; 20 | 21 | /** 22 | * Created by pengxr on 2019/12/17. 23 | */ 24 | public class SpacingDecoration extends RecyclerView.ItemDecoration { 25 | 26 | @NonNull 27 | private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 28 | // 内边距 29 | @Dimension 30 | private int mRowSpacing = 0; 31 | @Dimension 32 | private int mColumnSpacing = 0; 33 | // 四个外边距 34 | @Dimension 35 | private int mOutLeftSpacing = 0; 36 | @Dimension 37 | private int mOutTopSpacing = 0; 38 | @Dimension 39 | private int mOutRightSpacing = 0; 40 | @Dimension 41 | private int mOutBottomSpacing = 0; 42 | 43 | public SpacingDecoration() { 44 | } 45 | 46 | public SpacingDecoration(Context context, @Dimension(unit = DP) int inSpacingDp) { 47 | this(context, inSpacingDp, inSpacingDp, Color.TRANSPARENT); 48 | } 49 | 50 | public SpacingDecoration(Context context, @Dimension(unit = DP) int rowSpacingDp, @Dimension(unit = DP) int columnSpacingDp) { 51 | this(context, rowSpacingDp, columnSpacingDp, Color.TRANSPARENT); 52 | } 53 | 54 | public SpacingDecoration(Context context, 55 | @Dimension(unit = DP) int rowSpacingDp, 56 | @Dimension(unit = DP) int columnSpacingDp, @ColorInt int decorationColor) { 57 | mRowSpacing = DensityUtil.dip2px(context, rowSpacingDp); 58 | mColumnSpacing = DensityUtil.dip2px(context, columnSpacingDp); 59 | 60 | mPaint.setColor(decorationColor); 61 | } 62 | 63 | /** 64 | * 设置外边距 65 | */ 66 | public void setOutSpacing(Context context, 67 | @Dimension(unit = DP) int leftDp, 68 | @Dimension(unit = DP) int topDp, 69 | @Dimension(unit = DP) int rightDp, 70 | @Dimension(unit = DP) int bottomDp) { 71 | mOutLeftSpacing = DensityUtil.dip2px(context, leftDp); 72 | mOutTopSpacing = DensityUtil.dip2px(context, topDp); 73 | mOutRightSpacing = DensityUtil.dip2px(context, rightDp); 74 | mOutBottomSpacing = DensityUtil.dip2px(context, bottomDp); 75 | } 76 | 77 | public void setDecorationColor(@ColorInt int decorationColor) { 78 | mPaint.setColor(decorationColor); 79 | } 80 | 81 | // --------------------------------------------------------------------------------------------- 82 | 83 | @Override 84 | public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State _state) { 85 | // super 中将 outRect 四边置 0 86 | super.getItemOffsets(outRect, view, parent, _state); 87 | 88 | // 位置 89 | int position = parent.getChildAdapterPosition(view); 90 | // 总数 91 | int itemCount = parent.getAdapter().getItemCount(); 92 | if (position >= itemCount) { 93 | return; 94 | } 95 | 96 | if (parent.getLayoutManager() instanceof GridLayoutManager) { 97 | // 网格 98 | getItemOffsetForGrid(outRect, (GridLayoutManager) parent.getLayoutManager(), position, itemCount); 99 | } else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) { 100 | // 瀑布 101 | getItemOffsetForStagger(outRect, view, (StaggeredGridLayoutManager) parent.getLayoutManager(), position, itemCount); 102 | } else if (parent.getLayoutManager() instanceof LinearLayoutManager) { 103 | // 线性 104 | getItemOffsetsForLinear(outRect, (LinearLayoutManager) parent.getLayoutManager(), position, itemCount); 105 | } 106 | } 107 | 108 | /** 109 | * 处理线性布局 110 | */ 111 | private void getItemOffsetsForLinear(@NonNull Rect outRect, LinearLayoutManager lm, int position, int count) { 112 | if (lm.getOrientation() == LinearLayoutManager.HORIZONTAL) { 113 | // 横向 114 | outRect.top = mOutTopSpacing; 115 | outRect.bottom = mOutBottomSpacing; 116 | // 第一个 117 | if (0 == position) { 118 | outRect.left = mOutLeftSpacing; 119 | } 120 | // 内边距 121 | if (position > 0) { 122 | outRect.left = mColumnSpacing; 123 | } 124 | // 最后一个(只有一项item时,即是第一个也是最后一个) 125 | if (position == count - 1) { 126 | outRect.right = mOutRightSpacing; 127 | } 128 | } else { 129 | // 纵向 130 | outRect.left = mOutLeftSpacing; 131 | outRect.right = mOutRightSpacing; 132 | // 第一个 133 | if (0 == position) { 134 | outRect.top = mOutTopSpacing; 135 | } 136 | // 内边距 137 | if (position > 0) { 138 | outRect.top = mRowSpacing; 139 | } 140 | // 最后一个(只有一项item时,即是第一个也是最后一个) 141 | if (position == count - 1) { 142 | outRect.bottom = mOutBottomSpacing; 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * 处理网格布局 149 | */ 150 | private void getItemOffsetForGrid(@NonNull Rect outRect, @NonNull GridLayoutManager lm, int position, int count) { 151 | int spanCount = lm.getSpanCount(); 152 | int column = position % spanCount; 153 | getGridItemOffsets(outRect, position, count, column, spanCount, lm.getOrientation()); 154 | } 155 | 156 | /** 157 | * 处理瀑布流布局 158 | */ 159 | private void getItemOffsetForStagger(@NonNull Rect outRect, @NonNull View view, @NonNull StaggeredGridLayoutManager lm, int position, int count) { 160 | int spanCount = lm.getSpanCount(); 161 | StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); 162 | int column = lp.getSpanIndex(); 163 | getGridItemOffsets(outRect, position, count, column, spanCount, lm.getOrientation()); 164 | } 165 | 166 | private void getGridItemOffsets(Rect outRect, int position, int count, int column, int spanCount, int orientation) { 167 | if (0 == orientation) { 168 | // 横向 169 | if (position < spanCount) { 170 | outRect.left = mOutLeftSpacing; 171 | } 172 | if (count - position < spanCount) { 173 | outRect.right = mOutRightSpacing; 174 | } 175 | if (position >= spanCount) { 176 | outRect.left = mColumnSpacing; 177 | } 178 | if (0 == column) { 179 | outRect.top = mOutTopSpacing; 180 | } 181 | if (column >= 0 && column < spanCount - 1) { 182 | outRect.bottom = mRowSpacing / 2; // 误差? 183 | } 184 | if (column > 0 && column <= spanCount - 1) { 185 | outRect.top = mRowSpacing / 2; 186 | } 187 | if (column == spanCount - 1) { 188 | outRect.bottom = mOutBottomSpacing; 189 | } 190 | } else { 191 | // 纵向 192 | if (position < spanCount) { 193 | outRect.top = mOutTopSpacing; 194 | } 195 | if (position >= spanCount) { 196 | outRect.top = mRowSpacing; 197 | } 198 | if (count - position < spanCount) { 199 | outRect.bottom = mOutBottomSpacing; 200 | } 201 | if (0 == column) { 202 | outRect.left = mOutLeftSpacing; 203 | } 204 | if (column >= 0 && column < spanCount - 1) { 205 | outRect.right = mColumnSpacing / 2; // 误差? 206 | } 207 | if (column > 0 && column <= spanCount - 1) { 208 | outRect.left = mColumnSpacing / 2; 209 | } 210 | if (column == spanCount - 1) { 211 | outRect.right = mOutRightSpacing; 212 | } 213 | } 214 | } 215 | 216 | // @Override 217 | // public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 218 | // int left = parent.getPaddingLeft(); 219 | // int right = parent.getWidth() - parent.getPaddingRight(); 220 | // 221 | // int childCount = parent.getChildCount(); 222 | // for (int index = 0; index < childCount; index++) { 223 | // View child = parent.getChildAt(index); 224 | // RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); 225 | // int childPosition = layoutParams.getViewAdapterPosition(); 226 | // // 绘制分割线 227 | // left += layoutParams.leftMargin; 228 | // right -= layoutParams.rightMargin; 229 | // int top = child.getTop() - layoutParams.topMargin - mInjector.getGroupTitleHeight(childPosition); 230 | // int bottom = child.getTop() - layoutParams.topMargin; 231 | // canvas.drawRect(left, top, right, bottom, mPaint); 232 | // } 233 | // } 234 | } 235 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xhdpi/ic_back.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xhdpi/ic_share.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/ic_back.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/ic_launcher.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/ic_share.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/icon_baozi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_baozi.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/icon_bike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_bike.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/icon_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_cat.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/icon_guita.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_guita.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/icon_icecream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_icecream.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/icon_noodle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_noodle.jpg -------------------------------------------------------------------------------- /sample/src/main/res/layout/goods_detail_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/goods_detail_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 30 | 31 | 45 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 26 | 27 | 39 | 40 | 49 | 50 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/store_home_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 14 | 24 | 25 | 26 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/store_home_goods_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 25 | 26 | 42 | 43 | 59 | 60 | -------------------------------------------------------------------------------- /sample/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /sample/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | EasyTrack 3 | -------------------------------------------------------------------------------- /sample/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "EasyTrack" 2 | include ':sample' 3 | include ':easytrack' 4 | --------------------------------------------------------------------------------