├── .gitignore ├── README.MD ├── app ├── .gitignore ├── build.gradle ├── libs │ └── merge-release.aar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── onzhou │ │ └── module │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── onzhou │ │ │ └── module │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── onzhou │ └── module │ └── ExampleUnitTest.java ├── build.gradle ├── common ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── onzhou │ │ │ └── module │ │ │ └── common │ │ │ └── CommonActivity.java │ └── res │ │ ├── layout │ │ └── activity_common.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── onzhou │ └── module │ └── common │ └── ExampleUnitTest.java ├── config.gradle ├── download ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── onzhou │ │ │ └── module │ │ │ └── download │ │ │ └── DownloadActivity.java │ └── res │ │ ├── layout │ │ └── activity_download.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── onzhou │ └── module │ └── download │ └── ExampleUnitTest.java ├── fat-aar.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── liveroom ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── onzhou │ │ │ └── module │ │ │ └── liveroom │ │ │ └── LiveRoomActivity.java │ └── res │ │ ├── layout │ │ └── activity_live_room.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── onzhou │ └── module │ └── liveroom │ └── ExampleUnitTest.java ├── merge ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── onzhou │ └── module │ └── merge │ └── ExampleUnitTest.java ├── module.gradle ├── publish.gradle ├── settings.gradle ├── upload ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── onzhou │ │ │ └── module │ │ │ └── upload │ │ │ └── UploadActivity.java │ └── res │ │ ├── layout │ │ └── activity_upload.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── onzhou │ └── module │ └── upload │ └── ExampleUnitTest.java └── video ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── onzhou │ │ └── module │ │ └── video │ │ └── VideoPlayActivity.java └── res │ ├── layout │ └── activity_video_play.xml │ └── values │ └── strings.xml └── test └── java └── com └── onzhou └── module └── video └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | project.properties 22 | *.resources.prefs 23 | .settings* 24 | lint.xml 25 | 26 | # Log files 27 | *.log 28 | 29 | #ButterKnife 30 | .apt_generated/ 31 | .factorypath 32 | 33 | # for android-studio 34 | # Gradle files 35 | out/ 36 | outputs/ 37 | build/ 38 | .gradle/ 39 | */build/ 40 | gradlew 41 | gradlew.bat 42 | 43 | # IDEA 44 | *.iml 45 | .idea/ 46 | .idea/.name 47 | .idea/encodings.xml 48 | .idea/inspectionProfiles/Project_Default.xml 49 | .idea/inspectionProfiles/profiles_settings.xml 50 | .idea/misc.xml 51 | .idea/modules.xml 52 | .idea/scopes/scope_settings.xml 53 | .idea/vcs.xml 54 | .idea/workspace.xml 55 | .idea/libraries 56 | captures/ 57 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 逛掘金很久了,之前注册过邮箱账号,但是现在每次登录,都要求重置密码,改了也没用,很绝望,重新注册了一个账号。 2 | 今天第一次在掘金上分享,废话不多说,直接开始正题。 3 | 4 | - [概述]() 5 | - [合并的坑]() 6 | - [思考与扩展]() 7 | - [混淆配置]() 8 | - [小结]() 9 | 10 | ### 概述 11 | 12 | 简单介绍一下项目情况,笔者做这个项目快两年了,之所以有这篇文章,源于项目的需求,因为项目除了公司内部使用,`还需要抽取sdk给第三方合作公司使用,并且不同的合作方可能会对sdk作改动,A公司可能不要录屏功能,B公司可能只要视频播放功能,不要视频发布`,如何在不侵入我们主版本业务的情况下解决这个问题呢? 13 | 14 | 聪明的你肯定想到了,切分支呗! 15 | 16 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641d1a105b6c9c9?w=1043&h=349&f=png&s=9993) 17 | 18 | 这种方式只能解决一时之需,但是后续的主版本迭代,差异会越来越大,主版本同步到SDK的效率越来越低。 19 | 所以一旦是你采用了此种方案,我个人建议你做好跑路的准备! 20 | 21 | 所以去年底的时候,将组件化落地到了项目中,将各个模块独立,按照第三方的需要,实现灵活的搭配,这样一来,可以解耦我们的各个模块,便于维护,也可以适应第三方的定制需求,但是今天笔者讨论的并非组件化相关的内容,与该主题相关的内容很多,笔者今天想讨论的是组件化之后踩到的一些坑,,可能这些坑你永远也不会碰到,但是既然来了,看完又何妨呢? 22 | 23 | 目前项目架构如图所示: 24 | 25 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641d16854168988?w=710&h=674&f=png&s=26198) 26 | 27 | 组件化已基本完成,这才迈开了第一步,如何实现差异化呢? 28 | 29 | >1.分别打包 30 | 31 | 将各个独立的组件分别打包成对应的aar,提供给第三方,但是又涉及到一个问题,那就是混淆的问题,如果直接分别提供原始的aar包,那么源代码几乎等于完全暴露,如果分别混淆,又会存在一个问题,公共组件中常用的工具类被混淆,上层的短视频这些组件就会找不到对应的类。 32 | > 2.合并打包 33 | 34 | 这种方案具备良好的可行性,因为最终合并的文件只有一个,便于混淆,遗憾的是Android官方并没有提供这种合并的操作,但是发现github上有作者开源了一个合并脚本[fat-aar.gradle](https://github.com/adwiv/android-fat-aar),这个脚本的作用实际就是合并我们的多个组件为一个aar 35 | 36 | ### 合并的坑 37 | 38 | 下面笔者将用一个示例工程来演示合并的一些相关问题。 39 | [合并组件工程示例](https://github.com/byhook/merge-module) 40 | 41 | 用一张简单的图来描述其中的依赖关系 42 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641cd48f520d9de?w=447&h=399&f=png&s=6627) 43 | 44 | 最上层的是我们要生成的最终的merge.aar 45 | 他会合并直播间liveroom模块,合并video视频模块,而对应的模块也会依赖下层的组件,如何依赖合并呢? 46 | 47 | ``` 48 | apply from: "../fat-aar.gradle" 49 | ``` 50 | 51 | ``` 52 | embedded project(':common') 53 | embedded project(':upload') 54 | embedded project(':download') 55 | embedded project(':video') 56 | embedded project(':liveroom') 57 | ``` 58 | 59 | >注意: 需要合并的组件,只需要在最上层的组件中使用`embedded`关键字标记即可,并且下层所依赖的所有组件,都需要标记一次 60 | 61 | 接下来直接使用命令打包合并 62 | ``` 63 | cd merge 64 | gradle clean asR 65 | ``` 66 | 合并完成之后你以为就结束了吗?你太年轻了!!! 67 | 当你给别人使用的时候,马上就会发现第一个坑: 68 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641cb50b294ade6?w=1053&h=495&f=png&s=93451) 69 | 70 | 出现这个错误的原因,经过笔者肉眼的分析(各种google,各种stackoverflow),发现是由gradle 的插件版本引起的 71 | 72 | 笔者工作的环境 73 | ``` 74 | 系统: ubuntu 16.04 75 | gradle插件版本是2.3.3 76 | gradle的版本是3.5 77 | ``` 78 | 降级到gradle插件版本 79 | ``` 80 | classpath 'com.android.tools.build:gradle:2.2.3' 81 | ``` 82 | 此时编译直接报错: 83 | 84 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641cdf779b50010?w=1102&h=380&f=png&s=29311) 85 | 86 | 笔者用了一个比较笨的方法: 87 | 强行指定fat-aar.gradle脚本中的版本 88 | 89 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641ce11eed36582?w=595&h=177&f=png&s=17535) 90 | 91 | 终于合并完成!!! 92 | 但是不明白这两者合并出来的aar包差异在哪里,所以我将两个插件版本分别合并的aar包截图观察了一下 93 | 94 | `gradle2.3.3版本`合并的aar包 95 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641ccf9dc81d873?w=627&h=389&f=png&s=67879) 96 | `gradle2.2.3版本`合并的aar包 97 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641ccfb78bb354f?w=627&h=389&f=png&s=69681) 98 | 99 | 可以看到后者打包出来的aar文件,在libs目录中有一个jar包,这个jar包里存放的就是相关的R文件 100 | 101 | 所以解决上述问题的方案: 102 | >1.降级gradle插件版本到2.2.3版本,并修改对应脚本里的版本号 103 | 104 | >2.使用gradle插件版本2.2.3以上,但是需要手动修改fat-aar.gradle插件的内容,使之合并相关的R文件的jar包,这个问题大家也可以思考一下? 105 | 106 | >要知道,gradle的远程依赖功能实在是太方便了,我们可以很轻易的指定相关的依赖包,但是由于aar文件的特殊性,我们在组件中包含的一下远程依赖并不会被实际的合并到aar中去,例如你远程依赖了okhttp或者glide等相关的库,合并aar之后,就会出现如下的错误: 107 | 108 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641cf0d7434d3a9?w=1104&h=497&f=png&s=76637) 109 | 110 | 如何解决这个问题呢?聪明的你一定想到,`maven` 111 | 112 | 我们完全可以把这些依赖合并发布到maven中去,于是笔者尝试着搭建了[nexus私服](https://www.sonatype.com/download-oss-sonatype),具体的搭建不是本文讨论的重点。 113 | 114 | 幸运的是,fat-aar的作者给我们提供了相关的publish.gradle的脚本,真的不得不说,想什么来什么啊,既然有了现成的轮子,我们就直接跑呗! 115 | 116 | 在最上层的`merge`模块中添加依赖 117 | ``` 118 | apply from: '../publish.gradle' 119 | ``` 120 | 并添加如下配置 121 | ``` 122 | android { 123 | 124 | ... 125 | 126 | libraryVariants.all { variant -> 127 | variant.outputs.each { output -> 128 | def outputFile = output.outputFile 129 | if (outputFile != null && outputFile.name.endsWith('.aar')) { 130 | def fileName = getArtifactFileName() 131 | output.outputFile = new File(outputFile.parent, fileName) 132 | } 133 | } 134 | } 135 | 136 | } 137 | 138 | def getArtifactFileName() { 139 | return "${POM_ARTIFACT_ID}-${VERSION_NAME}.aar" 140 | } 141 | ``` 142 | 143 | 接下来配置自己的nexus私服即可 144 | ``` 145 | maven { 146 | //替换自己搭建的私服 147 | url "http://127.0.0.1:8081/nexus/content/repositories/releases" 148 | } 149 | ``` 150 | 151 | 通刚才的合并打包方式,最后发布到自己的nexus私服上 152 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641cf7fa54a42da?w=563&h=597&f=png&s=44646) 153 | 154 | 你以为这样就结束了吗?并没有!!! 155 | 156 | 实际操作过程中,笔者发现,我们本地实际是有依赖`本地第三方的aar包`的,换句话说,并非所有的库都是远程依赖,你会发现,原来脚本居然会将本地依赖的aar文件,也合并到pom.xml文件中,继而发布到nexus私服上去了,这个时候给别人远程依赖,就会一直找不到相关的本地库 157 | 158 | ![](https://user-gold-cdn.xitu.io/2018/6/20/1641d010b3505e39?w=460&h=365&f=png&s=41851) 159 | 160 | `如何解决呢?` 161 | 162 | 看来偷懒是不行的了,还是得改脚本,经过笔者的观察,发现在生成的pom.xml中可以过滤掉 163 | ``` 164 | pom.withXml { 165 | def dependenciesNode = asNode().appendNode('dependencies') 166 | 167 | depList.values().each { 168 | ResolvedDependency dep -> 169 | def hasGroup = dep.moduleGroup != null 170 | def hasName = (dep.moduleName != null && !"unspecified".equals(dep.moduleName) && !"".equals(dep.moduleVersion)) 171 | def hasVersion = (dep.moduleVersion != null && !"".equals(dep.moduleVersion) && !"unspecified".equals(dep.moduleVersion)) 172 | 173 | if (hasGroup && hasName && hasVersion) { 174 | def dependencyNode = dependenciesNode.appendNode('dependency') 175 | dependencyNode.appendNode('groupId', dep.moduleGroup) 176 | dependencyNode.appendNode('artifactId', dep.moduleName) 177 | dependencyNode.appendNode('version', dep.moduleVersion) 178 | } 179 | } 180 | } 181 | ``` 182 | 即把版本号为空的过滤掉即可。 183 | 184 | ### 思考与扩展 185 | 经过一番折腾,好歹也是合并出来我们想要的东西,但是笔者刚刚也说到了,公司主项目除了自己使用,还是组合成sdk给第三方使用,第三方可能会改下首页的布局,颜色,等等。如何在不侵入主业务的情况下,作变更呢?其实很简单,借鉴Android中多渠道包的生成,同名的资源放在不同的目录 186 | 遗憾的是原生的脚本并不支持这种姿势,我在最上层的`merge`模块中使用同名的资源试图覆盖下层的资源,达到替换的目的,并未得逞!!! 187 | 没办法,还是得改脚本,改动的思想实际就是在脚本合并的过程中,优先记录最上层的资源名称,当合并下层模块的资源文件时,直接跳过即可,改过的脚本在文章的末尾。 188 | 189 | ### 混淆配置 190 | 关于混淆的配置,只需要在最上层的merge模块中配置即可 191 | 192 | ### 注意事项 193 | >1.尽量不要使用原本的脚本文件,因为原作者已经几年未更新过,文章末尾有笔者的改动过的脚本文件 194 | 195 | >2.各个组件的清单会合并,不需要在最上层的组件中统一注册 196 | 197 | >3.本地依赖的jar包不用担心,因为脚本会合并到最终aar库的lib目录下 198 | 199 | >4.本地依赖的aar包,要记得随着远程依赖,给第三方一起依赖,即第三方除了依赖我们的远程依赖,还需要本地依赖我们所使用的aar文件,这也算是一个缺陷吧 200 | 201 | >5.第三方依赖的插件版本最好跟我们合并使用的gradle版本一致 202 | 203 | ### 小结 204 | 205 | 206 | 到目前为止,合并多组件的几个坑基本已经走了一遍了,其实在去年底,笔者在公司的直播项目中已经将组件化落地了,而后在实现多组件的道路上也踩了不少坑,本来这篇文章并没有打算发布出来,因为并不是所有人都会碰到这类需求,但是前段时间有个朋友公司问了我相关的问题,看他踩坑了很久,所以还是觉得发布出来,至少对于看到的人而言,以后碰到类似问题,可以少走些弯路,提高效率。 207 | 208 | `纸上得来终觉浅,绝知此事要躬行!` 209 | 210 | 211 | [示例项目](https://github.com/byhook/merge-module) 212 | 213 | [脚本地址](https://github.com/byhook/merge-gradle) 214 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | //构建版本 5 | compileSdkVersion rootProject.ext.android.compileSdkVersion 6 | buildToolsVersion rootProject.ext.android.buildToolsVersion 7 | //默认的配置 8 | defaultConfig { 9 | minSdkVersion rootProject.ext.android.minSdkVersion 10 | targetSdkVersion rootProject.ext.android.targetSdkVersion 11 | versionCode rootProject.ext.android.versionCode 12 | versionName rootProject.ext.android.versionName 13 | applicationId rootProject.ext.android.applicationId 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | repositories { 26 | jcenter() 27 | maven { 28 | //替换自己搭建的私服 29 | url uri("/home/byhook/android/maven") 30 | } 31 | } 32 | 33 | configurations.all { 34 | resolutionStrategy { 35 | force 'com.android.support:appcompat-v7:25.3.1' 36 | force 'com.android.support:support-annotations:25.3.1' 37 | } 38 | } 39 | 40 | dependencies { 41 | compile fileTree(dir: 'libs', include: ['*.jar']) 42 | compile 'com.android.support:appcompat-v7:25.3.1' 43 | testCompile 'junit:junit:4.12' 44 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' 45 | 46 | //先在merge目录下执行合并gradle clean asR,右侧栏目gradle选择发布到maven,再进行依赖 47 | compile 'com.onzhou.module:merge:1.0.1' 48 | } 49 | -------------------------------------------------------------------------------- /app/libs/merge-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byhook/merge-module/3f045c1e75ee63b397638654940226f5d859d728/app/libs/merge-release.aar -------------------------------------------------------------------------------- /app/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 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/onzhou/module/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.onzhou.module; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.onzhou.module", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/onzhou/module/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.onzhou.module; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | 7 | import com.onzhou.module.download.DownloadActivity; 8 | import com.onzhou.module.liveroom.LiveRoomActivity; 9 | import com.onzhou.module.upload.UploadActivity; 10 | import com.onzhou.module.video.VideoPlayActivity; 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_main); 18 | } 19 | 20 | public void onDownloadClick(View view) { 21 | DownloadActivity.intentStart(this); 22 | } 23 | 24 | public void onUploadClick(View view) { 25 | UploadActivity.intentStart(this); 26 | } 27 | 28 | 29 | public void onLiveClick(View view) { 30 | 31 | LiveRoomActivity.intentStart(this); 32 | } 33 | 34 | public void onVideoClick(View view) { 35 | VideoPlayActivity.intentStart(this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |