├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── teprinciple │ │ └── updateappdemo │ │ └── ExampleInstrumentedTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── teprinciple │ │ └── updateappdemo │ │ ├── CheckMd5DemoActivity.kt │ │ ├── JavaDemoActivity.java │ │ ├── MainActivity.kt │ │ └── SpanUtils.java │ └── res │ ├── drawable │ ├── bg_btn.xml │ ├── bg_custom_update.png │ ├── bg_custom_update_dialog.9.png │ ├── ic_close.png │ ├── ic_logo.png │ └── ic_update.png │ ├── layout │ ├── activity_java_demo.xml │ ├── activity_main.xml │ ├── check_md5_demo_activity.xml │ └── view_update_dialog_custom.xml │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── demo.png ├── update_ui_change.png ├── update_ui_custom.png ├── update_ui_downloading.png ├── update_ui_fail.png ├── update_ui_force.png ├── update_ui_plentiful.png └── update_ui_simple.png ├── readme ├── README_1.5.2.md ├── version.md └── 自定义UI.md ├── settings.gradle ├── update.gif ├── update.jks └── updateapputils ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── teprinciple │ └── library │ └── ExampleInstrumentedTest.java └── main ├── AndroidManifest.xml ├── java ├── constant │ ├── DownLoadBy.kt │ └── UiType.kt ├── extension │ ├── BooleanKtx.kt │ ├── ContextKtx.kt │ ├── CoreKtx.kt │ └── StringKtx.kt ├── listener │ ├── Md5CheckResultListener.kt │ ├── OnBtnClickListener.kt │ ├── OnInitUiListener.kt │ └── UpdateDownloadListener.kt ├── model │ ├── UiConfig.kt │ ├── UpdateConfig.kt │ └── UpdateInfo.kt ├── ui │ └── UpdateAppActivity.kt ├── update │ ├── DownloadAppUtils.kt │ ├── UpdateAppReceiver.kt │ ├── UpdateAppService.kt │ ├── UpdateAppUtils.kt │ └── UpdateFileProvider.kt └── util │ ├── AlertDialogUtil.kt │ ├── FileDownloadUtil.kt │ ├── GlobalContextProvider.kt │ ├── SPUtil.kt │ └── SignMd5Util.kt └── res ├── anim ├── dialog_enter.xml └── dialog_out.xml ├── drawable ├── bg_update_btn.xml ├── bg_update_dialog.xml └── ic_update_logo.png ├── layout ├── view_update_dialog_plentiful.xml └── view_update_dialog_simple.xml ├── values-en └── strings.xml ├── values ├── colors.xml ├── strings.xml └── styles.xml └── xml ├── network_security_config.xml └── update_file_paths.xml /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | # workflow的名称,会显示在github 的项目的Actions的右边列表中,如下图 2 | name: Android CI 3 | 4 | # 在满足以下条件触发这个workflow 5 | on: 6 | push: 7 | # 在指定的远程分支 master上,发生推送时 8 | branches: [ master ] 9 | 10 | jobs: 11 | # 多个job,如果有多个,每个以“-”开头,这里只有一个 job 12 | build: 13 | 14 | runs-on: ubuntu-latest # 该job 运行的系统环境,支持ubuntu 、windows、macOS 15 | 16 | # 下面是多个step ,每个以“-”开头 17 | steps: 18 | - uses: actions/checkout@v2 # step:检查分支 19 | - name: set up JDK 1.8 # step:设置jdk版本 20 | uses: actions/setup-java@v1 # 引用公共action 21 | with: 22 | java-version: 1.8 23 | - name: Build with Gradle # step:打包apk 24 | # 运行打包命令 25 | run: chmod +x gradlew &&./gradlew assembleRelease 26 | 27 | #step:上传apk 到action,在右上角查看 28 | # 官方文档 https://help.github.com/cn/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts#uploading-build-and-test-artifacts 29 | - name: Upload APK 30 | uses: actions/upload-artifact@v1 31 | with: 32 | name: app 33 | path: app/build/outputs/apk/release/app-release.apk 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UpdateAppUtils2.0 2 | 3 | [ ![](https://img.shields.io/badge/platform-android-green.svg) ](http://developer.android.com/index.html) 4 | [ ![Download](https://api.bintray.com/packages/teprinciple/maven/updateapputils/images/download.svg) ](https://bintray.com/teprinciple/maven/updateapputils/_latestVersion) 5 | 6 | ### 一行代码,快速实现app在线下载更新 A simple library for Android update app 7 | 8 | #### UpdateAppUtils2.0 特点 9 | * Kotlin First,Kotlin开发 10 | * 支持AndroidX 11 | * 支持Md5签名验证 12 | * 支持自定义任意UI 13 | * 适配中英文 14 | * 适配至Android 10 15 | * 通知栏图片自定义 16 | * 支持修改是否每次显示弹窗(非强更) 17 | * 支持安装完成后自动删除安装包 18 | 19 | UpdateAppUtils2.0功能结构变化巨大,建议使用2.0以上版本;[2.0以前版本文档](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/README_1.5.2.md) 20 | 21 | #### 效果图 22 | 23 | 24 | 25 | ### 集成 26 | 27 | ``` 28 | repositories { 29 | jcenter() 30 | } 31 | ``` 32 | 33 | Support 34 | ``` 35 | implementation 'com.teprinciple:updateapputils:2.3.0' 36 | ``` 37 | 38 | AndroidX项目 39 | ``` 40 | 注意,由于操作失误bintray 中updateapputilsX被我删掉, 41 | 所以2.3.0以后使用updateapputilsx。之前的仍使用updateapputilsX 42 | //implementation 'com.teprinciple:updateapputilsX:2.2.1' 43 | implementation 'com.teprinciple:updateapputilsx:2.3.0' 44 | 45 | ``` 46 | 47 | ### 使用 48 | 下面为kotlin使用示例,Java示例请参考[JavaDemo](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java) 49 | #### 1、快速使用 50 | 51 | ##### 注意:部分手机SDK内部初始化不了context,造成context空指针,建议在application或者使用SDK前先初始化 52 | ``` 53 | UpdateAppUtils.init(context) 54 | ``` 55 | 56 | ``` 57 | UpdateAppUtils 58 | .getInstance() 59 | .apkUrl(apkUrl) 60 | .updateTitle(updateTitle) 61 | .updateContent(updateContent) 62 | .update() 63 | ``` 64 | 65 | #### 2、多配置使用 66 | ``` 67 | // ui配置 68 | val uiConfig = UiConfig().apply { 69 | uiType = UiType.PLENTIFUL 70 | cancelBtnText = "下次再说" 71 | updateLogoImgRes = R.drawable.ic_update 72 | updateBtnBgRes = R.drawable.bg_btn 73 | titleTextColor = Color.BLACK 74 | titleTextSize = 18f 75 | contentTextColor = Color.parseColor("#88e16531") 76 | } 77 | 78 | // 更新配置 79 | val updateConfig = UpdateConfig().apply { 80 | force = true 81 | checkWifi = true 82 | needCheckMd5 = true 83 | isShowNotification = true 84 | notifyImgRes = R.drawable.ic_logo 85 | apkSavePath = Environment.getExternalStorageDirectory().absolutePath +"/teprinciple" 86 | apkSaveName = "teprinciple" 87 | } 88 | 89 | UpdateAppUtils 90 | .getInstance() 91 | .apkUrl(apkUrl) 92 | .updateTitle(updateTitle) 93 | .updateContent(updateContent) 94 | .updateConfig(updateConfig) 95 | .uiConfig(uiConfig) 96 | .setUpdateDownloadListener(object : UpdateDownloadListener{ 97 | // do something 98 | }) 99 | .update() 100 | ``` 101 | #### 3、md5校验说明 102 | 为了保证app的安全性,避免apk被二次打包的风险。UpdateAppUtils内部提供了对签名文件md5值校验的接口; 103 | 首先你需要保证当前应用和服务器apk使用同一个签名文件进行了签名(一定要保管好自己的签名文件,否则检验就失去了意义), 104 | 然后你需要将UpdateConfig 的 needCheckMd5 设置为true,并在Md5CheckResultListener监听中,监听校验返回结果。具体使用可参考 105 | [CheckMd5DemoActivity](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt) 106 | ``` 107 | UpdateAppUtils 108 | .getInstance() 109 | .apkUrl(apkUrl) 110 | .updateTitle(updateTitle) 111 | .updateContent(updateContent) 112 | .updateConfig(updateConfig) // needCheckMd5设置为true 113 | .setMd5CheckResultListener(object : Md5CheckResultListener{ 114 | override fun onResult(result: Boolean) { 115 | // true:检验通过,false:检验失败 116 | } 117 | }) 118 | ``` 119 | 120 | #### 4、自定义UI 121 | UpdateAppUtils内置了两套UI,你可以通过修改[UiConfig](#UiConfig)进行UI内容的自定义; 122 | 当然当内部UI模板与你期望UI差别很大时,你可以采用[完全自定义UI](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/%E8%87%AA%E5%AE%9A%E4%B9%89UI.md) 123 | 124 | ### Api说明 125 | #### 1、UpdateAppUtils Api 126 | 127 | | api | 说明 | 默认值 | 必须设置 | 128 | |:-------------- |:------------------------------------ |:--------------------- |:------ | 129 | | fun apkUrl(apkUrl: String)| 更新包服务器url | null | true | 130 | | fun update() | UpdateAppUtils入口 | - | true | 131 | | fun updateTitle(title: String) | 更新标题 | 版本更新啦! | false | 132 | | fun updateContent(content: String) | 更新内容 | 发现新版本,立即更新 | false | 133 | | fun updateConfig(config: UpdateConfig) | 更新配置 | 查看源码 | false | 134 | | fun uiConfig(uiConfig: UiConfig) | 更新弹窗UI配置 | 查看源码 | false | 135 | | fun setUpdateDownloadListener() | 下载过程监听 | null | false | 136 | | fun setMd5CheckResultListener() | md5校验结果回调 | null | false | 137 | | fun setOnInitUiListener() | 初始化更新弹窗UI回调 | null | false | 138 | | fun deleteInstalledApk() | 删除已安装的apk | - | false | 139 | | fun setCancelBtnClickListener() | 暂不更新按钮点击监听 | - | false | 140 | | fun setUpdateBtnClickListener() | 立即更新按钮点击监听 | - | false | 141 | 142 | #### 2、UpdateConfig:更新配置说明 143 | 144 | | 属性 | 说明 | 默认值 | 145 | |:--------------------- |:------------------------------------ |:------ | 146 | | isDebug | 是否输出【UpdateAppUtils】为Tag的日志| true | 147 | | force | 是否强制更新,强制时无取消按钮 | false | 148 | | apkSavePath | apk下载存放位置 | 包名目录 | 149 | | apkSaveName | apk保存文件名 | 项目名 | 150 | | downloadBy | 下载方式 | DownLoadBy.APP | 151 | | needCheckMd5 | 是否需要校验apk签名md5 | false | 152 | | checkWifi | 检查是否wifi | false | 153 | | isShowNotification | 是否显示通知栏进度 | true | 154 | | notifyImgRes | 通知栏图标 | 项目icon | 155 | | serverVersionName | 服务器上apk版本名 | 无 | 156 | | serverVersionCode | 服务器上apk版本号 | 无 | 157 | | alwaysShow | 是否每次显示更新弹窗(非强更) | true | 158 | | thisTimeShow | 本次是否显示更新弹窗(非强更) | false | 159 | | alwaysShowDownLoadDialog| 是否需要显示更新下载进度弹窗(非强更) | false | 160 | | showDownloadingToast | 开始下载时是否显示Toast | true | 161 | 162 | #### 3、UiConfig:更新弹窗Ui配置说明
163 | 164 | | 属性 | 说明 | 默认值 | 165 | |:--------------------- |:------------------------------------ |:------ | 166 | | uiType | ui模板 | UiType.SIMPLE | 167 | | customLayoutId | 自定义布局id | false | 168 | | updateLogoImgRes | 更新弹窗logo图片资源id | - | 169 | | titleTextSize | 标题字体大小 | 16sp | 170 | | titleTextColor | 标题字体颜色 | - | 171 | | contentTextSize | 内容字体大小 | 14sp | 172 | | contentTextColor | 内容字体颜色 | - | 173 | | updateBtnBgColor | 更新按钮背景颜色 | - | 174 | | updateBtnBgRes | 更新按钮背景资源id | - | 175 | | updateBtnTextColor | 更新按钮字体颜色 | - | 176 | | updateBtnTextSize | 更新按钮文字大小 | 14sp | 177 | | updateBtnText | 更新按钮文案 | 立即更新 | 178 | | cancelBtnBgColor | 取消按钮背景颜色 | - | 179 | | cancelBtnBgRes | 取消按钮背景资源id | - | 180 | | cancelBtnTextColor | 取消按钮文字颜色 | - | 181 | | cancelBtnTextSize | 取消按钮文字大小 | 14sp | 182 | | cancelBtnText | 取消按钮文案 | 暂不更新 | 183 | | downloadingToastText | 开始下载时的Toast提示文字 | 更新下载中... | 184 | | downloadingBtnText | 下载中 下载按钮以及通知栏标题前缀,进度自动拼接在后面 | 下载中 | 185 | | downloadFailText | 下载出错时,下载按钮及通知栏标题 | 下载出错,点击重试 | 186 | 187 | ### Demo体验 188 | 189 | 190 | ### 更新日志 191 | 192 | #### 2.3.0 193 | * 修复部分手机context空指针异常 194 | ##### [更多历史版本](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/version.md) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | defaultConfig { 8 | applicationId "com.example.teprinciple.updateappdemo" 9 | minSdkVersion 19 10 | targetSdkVersion 29 11 | versionCode 203 12 | versionName "2.0.3" 13 | } 14 | signingConfigs { 15 | config { 16 | storeFile file('../update.jks') 17 | storePassword '123456' 18 | keyAlias 'update' 19 | keyPassword '123456' 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | signingConfig signingConfigs.debug 27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 28 | } 29 | 30 | debug { 31 | minifyEnabled false 32 | signingConfig signingConfigs.config 33 | } 34 | } 35 | 36 | lintOptions { 37 | abortOnError false 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(include: ['*.jar'], dir: 'libs') 43 | implementation project(':updateapputils') 44 | implementation 'com.android.support:appcompat-v7:28.0.0' 45 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 46 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 47 | 48 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1' 49 | } 50 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\Teprinciple\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/teprinciple/updateappdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.teprinciple.updateappdemo; 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 | * Instrumentation 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.example.teprinciple.updateappdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.teprinciple.updateappdemo 2 | 3 | import android.os.Bundle 4 | import android.os.Environment 5 | import android.support.v7.app.AppCompatActivity 6 | import android.widget.Toast 7 | import kotlinx.android.synthetic.main.check_md5_demo_activity.* 8 | import listener.Md5CheckResultListener 9 | import model.UpdateConfig 10 | import update.UpdateAppUtils 11 | 12 | /** 13 | * desc: md5校验示例 14 | * time: 2019/7/1 15 | * @author yk 16 | */ 17 | class CheckMd5DemoActivity : AppCompatActivity() { 18 | 19 | /** 20 | * 已签名的apk 21 | */ 22 | private val signedApkUrl = "http://118.24.148.250:8080/yk/update_signed.apk" 23 | 24 | /** 25 | * 非正规签名的apk 26 | */ 27 | private val notSignedApkUrl = "http://118.24.148.250:8080/yk/update_not_signed.apk" 28 | 29 | private val updateTitle = "发现新版本V2.0.0" 30 | private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索" 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | 35 | setContentView(R.layout.check_md5_demo_activity) 36 | 37 | // 更新配置 38 | val updateConfig = UpdateConfig().apply { 39 | force = true 40 | needCheckMd5 = true 41 | } 42 | 43 | // 正确签名 44 | btn_signed.setOnClickListener { 45 | updateConfig.apply { apkSaveName = "signed" } 46 | UpdateAppUtils 47 | .getInstance() 48 | .apkUrl(signedApkUrl) 49 | .updateTitle(updateTitle) 50 | .updateContent(updateContent) 51 | .updateConfig(updateConfig) 52 | .setMd5CheckResultListener(object : Md5CheckResultListener { 53 | override fun onResult(result: Boolean) { 54 | Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show() 55 | } 56 | }) 57 | .update() 58 | } 59 | 60 | // 错误签名 61 | btn_not_signed.setOnClickListener { 62 | updateConfig.apply { apkSaveName = "not_signed" } 63 | UpdateAppUtils 64 | .getInstance() 65 | .apkUrl(notSignedApkUrl) 66 | .updateTitle(updateTitle) 67 | .updateContent(updateContent) 68 | .updateConfig(updateConfig) 69 | .setMd5CheckResultListener(object : Md5CheckResultListener { 70 | override fun onResult(result: Boolean) { 71 | Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show() 72 | } 73 | }) 74 | .update() 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.teprinciple.updateappdemo; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import constant.UiType; 11 | import listener.Md5CheckResultListener; 12 | import listener.UpdateDownloadListener; 13 | import model.UiConfig; 14 | import model.UpdateConfig; 15 | import update.UpdateAppUtils; 16 | 17 | /** 18 | * desc: java使用实例 19 | * time: 2019/6/27 20 | * @author yk 21 | */ 22 | public class JavaDemoActivity extends AppCompatActivity { 23 | 24 | private String apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk"; 25 | private String updateTitle = "发现新版本V2.0.0"; 26 | private String updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索"; 27 | 28 | @Override 29 | protected void onCreate(@Nullable Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_java_demo); 32 | 33 | UpdateAppUtils.init(this); 34 | 35 | findViewById(R.id.btn_java).setOnClickListener(new View.OnClickListener() { 36 | @Override 37 | public void onClick(View v) { 38 | 39 | UpdateConfig updateConfig = new UpdateConfig(); 40 | updateConfig.setCheckWifi(true); 41 | updateConfig.setNeedCheckMd5(true); 42 | updateConfig.setNotifyImgRes(R.drawable.ic_logo); 43 | 44 | UiConfig uiConfig = new UiConfig(); 45 | uiConfig.setUiType(UiType.PLENTIFUL); 46 | 47 | UpdateAppUtils 48 | .getInstance() 49 | .apkUrl(apkUrl) 50 | .updateTitle(updateTitle) 51 | .updateContent(updateContent) 52 | .uiConfig(uiConfig) 53 | .updateConfig(updateConfig) 54 | .setMd5CheckResultListener(new Md5CheckResultListener() { 55 | @Override 56 | public void onResult(boolean result) { 57 | 58 | } 59 | }) 60 | .setUpdateDownloadListener(new UpdateDownloadListener() { 61 | @Override 62 | public void onStart() { 63 | 64 | } 65 | 66 | @Override 67 | public void onDownload(int progress) { 68 | 69 | } 70 | 71 | @Override 72 | public void onFinish() { 73 | 74 | } 75 | 76 | @Override 77 | public void onError(@NotNull Throwable e) { 78 | 79 | } 80 | }) 81 | .update(); 82 | } 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/teprinciple/updateappdemo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.teprinciple.updateappdemo 2 | 3 | import android.content.Intent 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.os.Environment 7 | import android.support.v7.app.AppCompatActivity 8 | import android.view.View 9 | import android.widget.TextView 10 | import android.widget.Toast 11 | import constant.DownLoadBy 12 | import constant.UiType 13 | import kotlinx.android.synthetic.main.activity_main.* 14 | import listener.OnBtnClickListener 15 | import listener.OnInitUiListener 16 | import listener.UpdateDownloadListener 17 | import model.UiConfig 18 | import model.UpdateConfig 19 | import update.UpdateAppUtils 20 | 21 | 22 | class MainActivity : AppCompatActivity() { 23 | 24 | private val apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk" 25 | // private val apkUrl = "https://github.com/AlexLiuSheng/CheckVersionLib/blob/master/library/src/main/java/com/allenliu/versionchecklib/utils/AppUtils.java" 26 | private val updateTitle = "发现新版本V2.0.0" 27 | private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索" 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | setContentView(R.layout.activity_main) 32 | 33 | UpdateAppUtils.init(this) 34 | 35 | // 基本使用 36 | btn_basic_use.setOnClickListener { 37 | UpdateAppUtils 38 | .getInstance() 39 | .apkUrl(apkUrl) 40 | .updateTitle(updateTitle) 41 | .updateConfig(UpdateConfig(apkSaveName = "up_1.1")) 42 | .uiConfig(UiConfig(uiType = UiType.SIMPLE)) 43 | .updateContent(updateContent) 44 | .update() 45 | } 46 | 47 | // 浏览器下载 48 | btn_download_by_browser.setOnClickListener { 49 | 50 | // 使用SpannableString 51 | val content = SpanUtils(this) 52 | .appendLine("1、Kotlin重构版") 53 | .appendLine("2、支持自定义UI").setForegroundColor(Color.RED) 54 | .appendLine("3、增加md5校验").setForegroundColor(Color.parseColor("#88e16531")).setFontSize(20, true) 55 | .appendLine("4、更多功能等你探索").setBoldItalic() 56 | .appendLine().appendImage(R.mipmap.ic_launcher).setBoldItalic() 57 | .create() 58 | 59 | UpdateAppUtils 60 | .getInstance() 61 | .apkUrl(apkUrl) 62 | .updateTitle(updateTitle) 63 | .updateContent(content) 64 | .updateConfig(UpdateConfig().apply { 65 | downloadBy = DownLoadBy.BROWSER 66 | // alwaysShow = false 67 | serverVersionName = "2.0.0" 68 | }) 69 | .uiConfig(UiConfig(uiType = UiType.PLENTIFUL)) 70 | 71 | // 设置 取消 按钮点击事件 72 | .setCancelBtnClickListener(object : OnBtnClickListener { 73 | override fun onClick(): Boolean { 74 | Toast.makeText(this@MainActivity, "cancel btn click", Toast.LENGTH_SHORT).show() 75 | return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑 76 | } 77 | }) 78 | 79 | // 设置 立即更新 按钮点击事件 80 | .setUpdateBtnClickListener(object : OnBtnClickListener { 81 | override fun onClick(): Boolean { 82 | Toast.makeText(this@MainActivity, "update btn click", Toast.LENGTH_SHORT).show() 83 | return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑 84 | } 85 | }) 86 | 87 | .update() 88 | } 89 | 90 | // 自定义UI 91 | btn_custom_ui.setOnClickListener { 92 | UpdateAppUtils 93 | .getInstance() 94 | .apkUrl(apkUrl) 95 | .updateTitle(updateTitle) 96 | .updateContent(updateContent) 97 | .updateConfig(UpdateConfig(alwaysShowDownLoadDialog = true)) 98 | .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom)) 99 | .setOnInitUiListener(object : OnInitUiListener { 100 | override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) { 101 | view?.findViewById(R.id.tv_update_title)?.text = "版本更新啦" 102 | view?.findViewById(R.id.tv_version_name)?.text = "V2.0.0" 103 | // do more... 104 | } 105 | }) 106 | .update() 107 | } 108 | 109 | // java使用示例 110 | btn_java_sample.setOnClickListener { 111 | startActivity(Intent(this, JavaDemoActivity::class.java)) 112 | } 113 | 114 | // md5校验 115 | btn_check_md5.setOnClickListener { 116 | startActivity(Intent(this, CheckMd5DemoActivity::class.java)) 117 | } 118 | 119 | // 高级使用 120 | btn_higher_level_use.setOnClickListener { 121 | // ui配置 122 | val uiConfig = UiConfig().apply { 123 | uiType = UiType.PLENTIFUL 124 | cancelBtnText = "下次再说" 125 | updateLogoImgRes = R.drawable.ic_update 126 | updateBtnBgRes = R.drawable.bg_btn 127 | titleTextColor = Color.BLACK 128 | titleTextSize = 18f 129 | contentTextColor = Color.parseColor("#88e16531") 130 | } 131 | 132 | // 更新配置 133 | val updateConfig = UpdateConfig().apply { 134 | force = true 135 | isDebug = true 136 | checkWifi = true 137 | isShowNotification = true 138 | notifyImgRes = R.drawable.ic_logo 139 | apkSavePath = Environment.getExternalStorageDirectory().absolutePath + "/teprinciple" 140 | apkSaveName = "teprinciple" 141 | } 142 | 143 | UpdateAppUtils 144 | .getInstance() 145 | .apkUrl(apkUrl) 146 | .updateTitle(updateTitle) 147 | .updateContent(updateContent) 148 | .updateConfig(updateConfig) 149 | .uiConfig(uiConfig) 150 | .setUpdateDownloadListener(object : UpdateDownloadListener { 151 | override fun onStart() { 152 | } 153 | 154 | override fun onDownload(progress: Int) { 155 | } 156 | 157 | override fun onFinish() { 158 | } 159 | 160 | override fun onError(e: Throwable) { 161 | } 162 | }) 163 | .update() 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/teprinciple/updateappdemo/SpanUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.teprinciple.updateappdemo; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.BlurMaskFilter; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.Paint; 11 | import android.graphics.Path; 12 | import android.graphics.Rect; 13 | import android.graphics.Shader; 14 | import android.graphics.Typeface; 15 | import android.graphics.drawable.BitmapDrawable; 16 | import android.graphics.drawable.Drawable; 17 | import android.net.Uri; 18 | import android.support.annotation.ColorInt; 19 | import android.support.annotation.DrawableRes; 20 | import android.support.annotation.FloatRange; 21 | import android.support.annotation.IntDef; 22 | import android.support.annotation.IntRange; 23 | import android.support.annotation.NonNull; 24 | import android.support.annotation.Nullable; 25 | import android.support.v4.content.ContextCompat; 26 | import android.text.Layout; 27 | import android.text.Layout.Alignment; 28 | import android.text.SpannableStringBuilder; 29 | import android.text.Spanned; 30 | import android.text.TextPaint; 31 | import android.text.style.AbsoluteSizeSpan; 32 | import android.text.style.AlignmentSpan; 33 | import android.text.style.BackgroundColorSpan; 34 | import android.text.style.CharacterStyle; 35 | import android.text.style.ClickableSpan; 36 | import android.text.style.ForegroundColorSpan; 37 | import android.text.style.LeadingMarginSpan; 38 | import android.text.style.LineHeightSpan; 39 | import android.text.style.MaskFilterSpan; 40 | import android.text.style.RelativeSizeSpan; 41 | import android.text.style.ReplacementSpan; 42 | import android.text.style.ScaleXSpan; 43 | import android.text.style.StrikethroughSpan; 44 | import android.text.style.StyleSpan; 45 | import android.text.style.SubscriptSpan; 46 | import android.text.style.SuperscriptSpan; 47 | import android.text.style.TypefaceSpan; 48 | import android.text.style.URLSpan; 49 | import android.text.style.UnderlineSpan; 50 | import android.text.style.UpdateAppearance; 51 | import android.util.Log; 52 | 53 | import java.io.InputStream; 54 | import java.lang.annotation.Retention; 55 | import java.lang.annotation.RetentionPolicy; 56 | import java.lang.ref.WeakReference; 57 | 58 | import static android.graphics.BlurMaskFilter.Blur; 59 | 60 | /** 61 | *
  62 |  *     author: Blankj
  63 |  *     blog  : http://blankj.com
  64 |  *     usage : https://www.jianshu.com/p/509b0d2626f4
  65 |  *     time  : 16/12/13
  66 |  *     desc  : utils about span
  67 |  * 
68 | */ 69 | public final class SpanUtils { 70 | 71 | private static final int COLOR_DEFAULT = 0xFEFFFFFF; 72 | 73 | public static final int ALIGN_BOTTOM = 0; 74 | public static final int ALIGN_BASELINE = 1; 75 | public static final int ALIGN_CENTER = 2; 76 | public static final int ALIGN_TOP = 3; 77 | 78 | @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP}) 79 | @Retention(RetentionPolicy.SOURCE) 80 | public @interface Align { 81 | } 82 | 83 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 84 | 85 | private CharSequence mText; 86 | private int flag; 87 | private int foregroundColor; 88 | private int backgroundColor; 89 | private int lineHeight; 90 | private int alignLine; 91 | private int quoteColor; 92 | private int stripeWidth; 93 | private int quoteGapWidth; 94 | private int first; 95 | private int rest; 96 | private int bulletColor; 97 | private int bulletRadius; 98 | private int bulletGapWidth; 99 | private int fontSize; 100 | private boolean fontSizeIsDp; 101 | private float proportion; 102 | private float xProportion; 103 | private boolean isStrikethrough; 104 | private boolean isUnderline; 105 | private boolean isSuperscript; 106 | private boolean isSubscript; 107 | private boolean isBold; 108 | private boolean isItalic; 109 | private boolean isBoldItalic; 110 | private String fontFamily; 111 | private Typeface typeface; 112 | private Alignment alignment; 113 | private ClickableSpan clickSpan; 114 | private String url; 115 | private float blurRadius; 116 | private Blur style; 117 | private Shader shader; 118 | private float shadowRadius; 119 | private float shadowDx; 120 | private float shadowDy; 121 | private int shadowColor; 122 | private Object[] spans; 123 | 124 | private Bitmap imageBitmap; 125 | private Drawable imageDrawable; 126 | private Uri imageUri; 127 | private int imageResourceId; 128 | private int alignImage; 129 | 130 | private int spaceSize; 131 | private int spaceColor; 132 | 133 | private SpannableStringBuilder mBuilder; 134 | 135 | private int mType; 136 | private final int mTypeCharSequence = 0; 137 | private final int mTypeImage = 1; 138 | private final int mTypeSpace = 2; 139 | 140 | private Context mContext; 141 | 142 | public SpanUtils(Context context) { 143 | mContext = context.getApplicationContext(); 144 | mBuilder = new SpannableStringBuilder(); 145 | mText = ""; 146 | setDefault(); 147 | } 148 | 149 | private void setDefault() { 150 | flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; 151 | foregroundColor = COLOR_DEFAULT; 152 | backgroundColor = COLOR_DEFAULT; 153 | lineHeight = -1; 154 | quoteColor = COLOR_DEFAULT; 155 | first = -1; 156 | bulletColor = COLOR_DEFAULT; 157 | fontSize = -1; 158 | proportion = -1; 159 | xProportion = -1; 160 | isStrikethrough = false; 161 | isUnderline = false; 162 | isSuperscript = false; 163 | isSubscript = false; 164 | isBold = false; 165 | isItalic = false; 166 | isBoldItalic = false; 167 | fontFamily = null; 168 | typeface = null; 169 | alignment = null; 170 | clickSpan = null; 171 | url = null; 172 | blurRadius = -1; 173 | shader = null; 174 | shadowRadius = -1; 175 | spans = null; 176 | 177 | imageBitmap = null; 178 | imageDrawable = null; 179 | imageUri = null; 180 | imageResourceId = -1; 181 | 182 | spaceSize = -1; 183 | } 184 | 185 | /** 186 | * Set the span of flag. 187 | * 188 | * @param flag 189 | * The flag. 190 | *
    191 | *
  • {@link Spanned#SPAN_INCLUSIVE_EXCLUSIVE}
  • 192 | *
  • {@link Spanned#SPAN_INCLUSIVE_INCLUSIVE}
  • 193 | *
  • {@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE}
  • 194 | *
  • {@link Spanned#SPAN_EXCLUSIVE_INCLUSIVE}
  • 195 | *
196 | * @return the single {@link SpanUtils} instance 197 | */ 198 | public SpanUtils setFlag(final int flag) { 199 | this.flag = flag; 200 | return this; 201 | } 202 | 203 | /** 204 | * Set the span of foreground's color. 205 | * 206 | * @param color 207 | * The color of foreground 208 | * @return the single {@link SpanUtils} instance 209 | */ 210 | public SpanUtils setForegroundColor(@ColorInt final int color) { 211 | this.foregroundColor = color; 212 | return this; 213 | } 214 | 215 | /** 216 | * Set the span of background's color. 217 | * 218 | * @param color 219 | * The color of background 220 | * @return the single {@link SpanUtils} instance 221 | */ 222 | public SpanUtils setBackgroundColor(@ColorInt final int color) { 223 | this.backgroundColor = color; 224 | return this; 225 | } 226 | 227 | /** 228 | * Set the span of line height. 229 | * 230 | * @param lineHeight 231 | * The line height, in pixel. 232 | * @return the single {@link SpanUtils} instance 233 | */ 234 | public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight) { 235 | return setLineHeight(lineHeight, ALIGN_CENTER); 236 | } 237 | 238 | /** 239 | * Set the span of line height. 240 | * 241 | * @param lineHeight 242 | * The line height, in pixel. 243 | * @param align 244 | * The alignment. 245 | *
    246 | *
  • {@link Align#ALIGN_TOP }
  • 247 | *
  • {@link Align#ALIGN_CENTER}
  • 248 | *
  • {@link Align#ALIGN_BOTTOM}
  • 249 | *
250 | * @return the single {@link SpanUtils} instance 251 | */ 252 | public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight, 253 | @Align final int align) { 254 | this.lineHeight = lineHeight; 255 | this.alignLine = align; 256 | return this; 257 | } 258 | 259 | /** 260 | * Set the span of quote's color. 261 | * 262 | * @param color 263 | * The color of quote 264 | * @return the single {@link SpanUtils} instance 265 | */ 266 | public SpanUtils setQuoteColor(@ColorInt final int color) { 267 | return setQuoteColor(color, 2, 2); 268 | } 269 | 270 | /** 271 | * Set the span of quote's color. 272 | * 273 | * @param color 274 | * The color of quote. 275 | * @param stripeWidth 276 | * The width of stripe, in pixel. 277 | * @param gapWidth 278 | * The width of gap, in pixel. 279 | * @return the single {@link SpanUtils} instance 280 | */ 281 | public SpanUtils setQuoteColor(@ColorInt final int color, 282 | @IntRange(from = 1) final int stripeWidth, 283 | @IntRange(from = 0) final int gapWidth) { 284 | this.quoteColor = color; 285 | this.stripeWidth = stripeWidth; 286 | this.quoteGapWidth = gapWidth; 287 | return this; 288 | } 289 | 290 | /** 291 | * Set the span of leading margin. 292 | * 293 | * @param first 294 | * The indent for the first line of the paragraph. 295 | * @param rest 296 | * The indent for the remaining lines of the paragraph. 297 | * @return the single {@link SpanUtils} instance 298 | */ 299 | public SpanUtils setLeadingMargin(@IntRange(from = 0) final int first, 300 | @IntRange(from = 0) final int rest) { 301 | this.first = first; 302 | this.rest = rest; 303 | return this; 304 | } 305 | 306 | /** 307 | * Set the span of bullet. 308 | * 309 | * @param gapWidth 310 | * The width of gap, in pixel. 311 | * @return the single {@link SpanUtils} instance 312 | */ 313 | public SpanUtils setBullet(@IntRange(from = 0) final int gapWidth) { 314 | return setBullet(0, 3, gapWidth); 315 | } 316 | 317 | /** 318 | * Set the span of bullet. 319 | * 320 | * @param color 321 | * The color of bullet. 322 | * @param radius 323 | * The radius of bullet, in pixel. 324 | * @param gapWidth 325 | * The width of gap, in pixel. 326 | * @return the single {@link SpanUtils} instance 327 | */ 328 | public SpanUtils setBullet(@ColorInt final int color, 329 | @IntRange(from = 0) final int radius, 330 | @IntRange(from = 0) final int gapWidth) { 331 | this.bulletColor = color; 332 | this.bulletRadius = radius; 333 | this.bulletGapWidth = gapWidth; 334 | return this; 335 | } 336 | 337 | /** 338 | * Set the span of font's size. 339 | * 340 | * @param size 341 | * The size of font. 342 | * @return the single {@link SpanUtils} instance 343 | */ 344 | public SpanUtils setFontSize(@IntRange(from = 0) final int size) { 345 | return setFontSize(size, false); 346 | } 347 | 348 | /** 349 | * Set the span of size of font. 350 | * 351 | * @param size 352 | * The size of font. 353 | * @param isSp 354 | * True to use sp, false to use pixel. 355 | * @return the single {@link SpanUtils} instance 356 | */ 357 | public SpanUtils setFontSize(@IntRange(from = 0) final int size, final boolean isSp) { 358 | this.fontSize = size; 359 | this.fontSizeIsDp = isSp; 360 | return this; 361 | } 362 | 363 | /** 364 | * Set the span of proportion of font. 365 | * 366 | * @param proportion 367 | * The proportion of font. 368 | * @return the single {@link SpanUtils} instance 369 | */ 370 | public SpanUtils setFontProportion(final float proportion) { 371 | this.proportion = proportion; 372 | return this; 373 | } 374 | 375 | /** 376 | * Set the span of transverse proportion of font. 377 | * 378 | * @param proportion 379 | * The transverse proportion of font. 380 | * @return the single {@link SpanUtils} instance 381 | */ 382 | public SpanUtils setFontXProportion(final float proportion) { 383 | this.xProportion = proportion; 384 | return this; 385 | } 386 | 387 | /** 388 | * Set the span of strikethrough. 389 | * 390 | * @return the single {@link SpanUtils} instance 391 | */ 392 | public SpanUtils setStrikethrough() { 393 | this.isStrikethrough = true; 394 | return this; 395 | } 396 | 397 | /** 398 | * Set the span of underline. 399 | * 400 | * @return the single {@link SpanUtils} instance 401 | */ 402 | public SpanUtils setUnderline() { 403 | this.isUnderline = true; 404 | return this; 405 | } 406 | 407 | /** 408 | * Set the span of superscript. 409 | * 410 | * @return the single {@link SpanUtils} instance 411 | */ 412 | public SpanUtils setSuperscript() { 413 | this.isSuperscript = true; 414 | return this; 415 | } 416 | 417 | /** 418 | * Set the span of subscript. 419 | * 420 | * @return the single {@link SpanUtils} instance 421 | */ 422 | public SpanUtils setSubscript() { 423 | this.isSubscript = true; 424 | return this; 425 | } 426 | 427 | /** 428 | * Set the span of bold. 429 | * 430 | * @return the single {@link SpanUtils} instance 431 | */ 432 | public SpanUtils setBold() { 433 | isBold = true; 434 | return this; 435 | } 436 | 437 | /** 438 | * Set the span of bold. 439 | * 440 | * @return the single {@link SpanUtils} instance 441 | */ 442 | public SpanUtils setNotBold() { 443 | isBold = false; 444 | return this; 445 | } 446 | 447 | /** 448 | * Set the span of italic. 449 | * 450 | * @return the single {@link SpanUtils} instance 451 | */ 452 | public SpanUtils setItalic() { 453 | isItalic = true; 454 | return this; 455 | } 456 | 457 | /** 458 | * Set the span of bold italic. 459 | * 460 | * @return the single {@link SpanUtils} instance 461 | */ 462 | public SpanUtils setBoldItalic() { 463 | isBoldItalic = true; 464 | return this; 465 | } 466 | 467 | /** 468 | * Set the span of font family. 469 | * 470 | * @param fontFamily 471 | * The font family. 472 | *
    473 | *
  • monospace
  • 474 | *
  • serif
  • 475 | *
  • sans-serif
  • 476 | *
477 | * @return the single {@link SpanUtils} instance 478 | */ 479 | public SpanUtils setFontFamily(@NonNull final String fontFamily) { 480 | this.fontFamily = fontFamily; 481 | return this; 482 | } 483 | 484 | /** 485 | * Set the span of typeface. 486 | * 487 | * @param typeface 488 | * The typeface. 489 | * @return the single {@link SpanUtils} instance 490 | */ 491 | public SpanUtils setTypeface(@NonNull final Typeface typeface) { 492 | this.typeface = typeface; 493 | return this; 494 | } 495 | 496 | /** 497 | * Set the span of alignment. 498 | * 499 | * @param alignment 500 | * The alignment. 501 | *
    502 | *
  • {@link Alignment#ALIGN_NORMAL }
  • 503 | *
  • {@link Alignment#ALIGN_OPPOSITE}
  • 504 | *
  • {@link Alignment#ALIGN_CENTER }
  • 505 | *
506 | * @return the single {@link SpanUtils} instance 507 | */ 508 | public SpanUtils setAlign(@NonNull final Alignment alignment) { 509 | this.alignment = alignment; 510 | return this; 511 | } 512 | 513 | /** 514 | * Set the span of click. 515 | *

Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}

516 | * 517 | * @param clickSpan 518 | * The span of click. 519 | * @return the single {@link SpanUtils} instance 520 | */ 521 | public SpanUtils setClickSpan(@NonNull final ClickableSpan clickSpan) { 522 | this.clickSpan = clickSpan; 523 | return this; 524 | } 525 | 526 | /** 527 | * Set the span of url. 528 | *

Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}

529 | * 530 | * @param url 531 | * The url. 532 | * @return the single {@link SpanUtils} instance 533 | */ 534 | public SpanUtils setUrl(@NonNull final String url) { 535 | this.url = url; 536 | return this; 537 | } 538 | 539 | /** 540 | * Set the span of blur. 541 | * 542 | * @param radius 543 | * The radius of blur. 544 | * @param style 545 | * The style. 546 | *
    547 | *
  • {@link Blur#NORMAL}
  • 548 | *
  • {@link Blur#SOLID}
  • 549 | *
  • {@link Blur#OUTER}
  • 550 | *
  • {@link Blur#INNER}
  • 551 | *
552 | * @return the single {@link SpanUtils} instance 553 | */ 554 | public SpanUtils setBlur(@FloatRange(from = 0, fromInclusive = false) final float radius, 555 | final Blur style) { 556 | this.blurRadius = radius; 557 | this.style = style; 558 | return this; 559 | } 560 | 561 | /** 562 | * Set the span of shader. 563 | * 564 | * @param shader 565 | * The shader. 566 | * @return the single {@link SpanUtils} instance 567 | */ 568 | public SpanUtils setShader(@NonNull final Shader shader) { 569 | this.shader = shader; 570 | return this; 571 | } 572 | 573 | /** 574 | * Set the span of shadow. 575 | * 576 | * @param radius 577 | * The radius of shadow. 578 | * @param dx 579 | * X-axis offset, in pixel. 580 | * @param dy 581 | * Y-axis offset, in pixel. 582 | * @param shadowColor 583 | * The color of shadow. 584 | * @return the single {@link SpanUtils} instance 585 | */ 586 | public SpanUtils setShadow(@FloatRange(from = 0, fromInclusive = false) final float radius, 587 | final float dx, 588 | final float dy, 589 | final int shadowColor) { 590 | this.shadowRadius = radius; 591 | this.shadowDx = dx; 592 | this.shadowDy = dy; 593 | this.shadowColor = shadowColor; 594 | return this; 595 | } 596 | 597 | 598 | /** 599 | * Set the spans. 600 | * 601 | * @param spans 602 | * The spans. 603 | * @return the single {@link SpanUtils} instance 604 | */ 605 | public SpanUtils setSpans(@NonNull final Object... spans) { 606 | if (spans.length > 0) { 607 | this.spans = spans; 608 | } 609 | return this; 610 | } 611 | 612 | /** 613 | * Append the text text. 614 | * 615 | * @param text 616 | * The text. 617 | * @return the single {@link SpanUtils} instance 618 | */ 619 | public SpanUtils append(@NonNull final CharSequence text) { 620 | apply(mTypeCharSequence); 621 | mText = text; 622 | return this; 623 | } 624 | 625 | /** 626 | * Append one line. 627 | * 628 | * @return the single {@link SpanUtils} instance 629 | */ 630 | public SpanUtils appendLine() { 631 | apply(mTypeCharSequence); 632 | mText = LINE_SEPARATOR; 633 | return this; 634 | } 635 | 636 | /** 637 | * Append text and one line. 638 | * 639 | * @return the single {@link SpanUtils} instance 640 | */ 641 | public SpanUtils appendLine(@NonNull final CharSequence text) { 642 | apply(mTypeCharSequence); 643 | mText = text + LINE_SEPARATOR; 644 | return this; 645 | } 646 | 647 | /** 648 | * Append one image. 649 | * 650 | * @param bitmap 651 | * The bitmap of image. 652 | * @return the single {@link SpanUtils} instance 653 | */ 654 | public SpanUtils appendImage(@NonNull final Bitmap bitmap) { 655 | return appendImage(bitmap, ALIGN_BOTTOM); 656 | } 657 | 658 | /** 659 | * Append one image. 660 | * 661 | * @param bitmap 662 | * The bitmap. 663 | * @param align 664 | * The alignment. 665 | *
    666 | *
  • {@link Align#ALIGN_TOP }
  • 667 | *
  • {@link Align#ALIGN_CENTER }
  • 668 | *
  • {@link Align#ALIGN_BASELINE}
  • 669 | *
  • {@link Align#ALIGN_BOTTOM }
  • 670 | *
671 | * @return the single {@link SpanUtils} instance 672 | */ 673 | public SpanUtils appendImage(@NonNull final Bitmap bitmap, @Align final int align) { 674 | apply(mTypeImage); 675 | this.imageBitmap = bitmap; 676 | this.alignImage = align; 677 | return this; 678 | } 679 | 680 | /** 681 | * Append one image. 682 | * 683 | * @param drawable 684 | * The drawable of image. 685 | * @return the single {@link SpanUtils} instance 686 | */ 687 | public SpanUtils appendImage(@NonNull final Drawable drawable) { 688 | return appendImage(drawable, ALIGN_BOTTOM); 689 | } 690 | 691 | /** 692 | * Append one image. 693 | * 694 | * @param drawable 695 | * The drawable of image. 696 | * @param align 697 | * The alignment. 698 | *
    699 | *
  • {@link Align#ALIGN_TOP }
  • 700 | *
  • {@link Align#ALIGN_CENTER }
  • 701 | *
  • {@link Align#ALIGN_BASELINE}
  • 702 | *
  • {@link Align#ALIGN_BOTTOM }
  • 703 | *
704 | * @return the single {@link SpanUtils} instance 705 | */ 706 | public SpanUtils appendImage(@NonNull final Drawable drawable, @Align final int align) { 707 | apply(mTypeImage); 708 | this.imageDrawable = drawable; 709 | this.alignImage = align; 710 | return this; 711 | } 712 | 713 | /** 714 | * Append one image. 715 | * 716 | * @param uri 717 | * The uri of image. 718 | * @return the single {@link SpanUtils} instance 719 | */ 720 | public SpanUtils appendImage(@NonNull final Uri uri) { 721 | return appendImage(uri, ALIGN_BOTTOM); 722 | } 723 | 724 | /** 725 | * Append one image. 726 | * 727 | * @param uri 728 | * The uri of image. 729 | * @param align 730 | * The alignment. 731 | *
    732 | *
  • {@link Align#ALIGN_TOP }
  • 733 | *
  • {@link Align#ALIGN_CENTER }
  • 734 | *
  • {@link Align#ALIGN_BASELINE}
  • 735 | *
  • {@link Align#ALIGN_BOTTOM }
  • 736 | *
737 | * @return the single {@link SpanUtils} instance 738 | */ 739 | public SpanUtils appendImage(@NonNull final Uri uri, @Align final int align) { 740 | apply(mTypeImage); 741 | this.imageUri = uri; 742 | this.alignImage = align; 743 | return this; 744 | } 745 | 746 | /** 747 | * Append one image. 748 | * 749 | * @param resourceId 750 | * The resource id of image. 751 | * @return the single {@link SpanUtils} instance 752 | */ 753 | public SpanUtils appendImage(@DrawableRes final int resourceId) { 754 | return appendImage(resourceId, ALIGN_BOTTOM); 755 | } 756 | 757 | /** 758 | * Append one image. 759 | * 760 | * @param resourceId 761 | * The resource id of image. 762 | * @param align 763 | * The alignment. 764 | *
    765 | *
  • {@link Align#ALIGN_TOP }
  • 766 | *
  • {@link Align#ALIGN_CENTER }
  • 767 | *
  • {@link Align#ALIGN_BASELINE}
  • 768 | *
  • {@link Align#ALIGN_BOTTOM }
  • 769 | *
770 | * @return the single {@link SpanUtils} instance 771 | */ 772 | public SpanUtils appendImage(@DrawableRes final int resourceId, @Align final int align) { 773 | append(Character.toString((char) 0));// it's important for span start with image 774 | apply(mTypeImage); 775 | this.imageResourceId = resourceId; 776 | this.alignImage = align; 777 | return this; 778 | } 779 | 780 | /** 781 | * Append space. 782 | * 783 | * @param size 784 | * The size of space. 785 | * @return the single {@link SpanUtils} instance 786 | */ 787 | public SpanUtils appendSpace(@IntRange(from = 0) final int size) { 788 | return appendSpace(size, Color.TRANSPARENT); 789 | } 790 | 791 | /** 792 | * Append space. 793 | * 794 | * @param size 795 | * The size of space. 796 | * @param color 797 | * The color of space. 798 | * @return the single {@link SpanUtils} instance 799 | */ 800 | public SpanUtils appendSpace(@IntRange(from = 0) final int size, @ColorInt final int color) { 801 | apply(mTypeSpace); 802 | spaceSize = size; 803 | spaceColor = color; 804 | return this; 805 | } 806 | 807 | private void apply(final int type) { 808 | applyLast(); 809 | mType = type; 810 | } 811 | 812 | /** 813 | * Create the span string. 814 | * 815 | * @return the span string 816 | */ 817 | public SpannableStringBuilder create() { 818 | applyLast(); 819 | return mBuilder; 820 | } 821 | 822 | private void applyLast() { 823 | if (mType == mTypeCharSequence) { 824 | updateCharCharSequence(); 825 | } else if (mType == mTypeImage) { 826 | updateImage(); 827 | } else if (mType == mTypeSpace) { 828 | updateSpace(); 829 | } 830 | setDefault(); 831 | } 832 | 833 | private void updateCharCharSequence() { 834 | if (mText.length() == 0) return; 835 | int start = mBuilder.length(); 836 | mBuilder.append(mText); 837 | int end = mBuilder.length(); 838 | if (foregroundColor != COLOR_DEFAULT) { 839 | mBuilder.setSpan(new ForegroundColorSpan(foregroundColor), start, end, flag); 840 | } 841 | if (backgroundColor != COLOR_DEFAULT) { 842 | mBuilder.setSpan(new BackgroundColorSpan(backgroundColor), start, end, flag); 843 | } 844 | if (first != -1) { 845 | mBuilder.setSpan(new LeadingMarginSpan.Standard(first, rest), start, end, flag); 846 | } 847 | if (quoteColor != COLOR_DEFAULT) { 848 | mBuilder.setSpan( 849 | new CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth), 850 | start, 851 | end, 852 | flag 853 | ); 854 | } 855 | if (bulletColor != COLOR_DEFAULT) { 856 | mBuilder.setSpan( 857 | new CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth), 858 | start, 859 | end, 860 | flag 861 | ); 862 | } 863 | // if (imGapWidth != -1) { 864 | // if (imBitmap != null) { 865 | // mBuilder.setSpan( 866 | // new CustomIconMarginSpan(imBitmap, imGapWidth, imAlign), 867 | // start, 868 | // end, 869 | // flag 870 | // ); 871 | // } else if (imDrawable != null) { 872 | // mBuilder.setSpan( 873 | // new CustomIconMarginSpan(imDrawable, imGapWidth, imAlign), 874 | // start, 875 | // end, 876 | // flag 877 | // ); 878 | // } else if (imUri != null) { 879 | // mBuilder.setSpan( 880 | // new CustomIconMarginSpan(imUri, imGapWidth, imAlign), 881 | // start, 882 | // end, 883 | // flag 884 | // ); 885 | // } else if (imResourceId != -1) { 886 | // mBuilder.setSpan( 887 | // new CustomIconMarginSpan(imResourceId, imGapWidth, imAlign), 888 | // start, 889 | // end, 890 | // flag 891 | // ); 892 | // } 893 | // } 894 | if (fontSize != -1) { 895 | mBuilder.setSpan(new AbsoluteSizeSpan(fontSize, fontSizeIsDp), start, end, flag); 896 | } 897 | if (proportion != -1) { 898 | mBuilder.setSpan(new RelativeSizeSpan(proportion), start, end, flag); 899 | } 900 | if (xProportion != -1) { 901 | mBuilder.setSpan(new ScaleXSpan(xProportion), start, end, flag); 902 | } 903 | if (lineHeight != -1) { 904 | mBuilder.setSpan(new CustomLineHeightSpan(lineHeight, alignLine), start, end, flag); 905 | } 906 | if (isStrikethrough) { 907 | mBuilder.setSpan(new StrikethroughSpan(), start, end, flag); 908 | } 909 | if (isUnderline) { 910 | mBuilder.setSpan(new UnderlineSpan(), start, end, flag); 911 | } 912 | if (isSuperscript) { 913 | mBuilder.setSpan(new SuperscriptSpan(), start, end, flag); 914 | } 915 | if (isSubscript) { 916 | mBuilder.setSpan(new SubscriptSpan(), start, end, flag); 917 | } 918 | if (isBold) { 919 | mBuilder.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag); 920 | } 921 | if (isItalic) { 922 | mBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag); 923 | } 924 | if (isBoldItalic) { 925 | mBuilder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flag); 926 | } 927 | if (fontFamily != null) { 928 | mBuilder.setSpan(new TypefaceSpan(fontFamily), start, end, flag); 929 | } 930 | if (typeface != null) { 931 | mBuilder.setSpan(new CustomTypefaceSpan(typeface), start, end, flag); 932 | } 933 | if (alignment != null) { 934 | mBuilder.setSpan(new AlignmentSpan.Standard(alignment), start, end, flag); 935 | } 936 | if (clickSpan != null) { 937 | mBuilder.setSpan(clickSpan, start, end, flag); 938 | } 939 | if (url != null) { 940 | mBuilder.setSpan(new URLSpan(url), start, end, flag); 941 | } 942 | if (blurRadius != -1) { 943 | mBuilder.setSpan( 944 | new MaskFilterSpan(new BlurMaskFilter(blurRadius, style)), 945 | start, 946 | end, 947 | flag 948 | ); 949 | } 950 | if (shader != null) { 951 | mBuilder.setSpan(new ShaderSpan(shader), start, end, flag); 952 | } 953 | if (shadowRadius != -1) { 954 | mBuilder.setSpan( 955 | new ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor), 956 | start, 957 | end, 958 | flag 959 | ); 960 | } 961 | if (spans != null) { 962 | for (Object span : spans) { 963 | mBuilder.setSpan(span, start, end, flag); 964 | } 965 | } 966 | } 967 | 968 | private void updateImage() { 969 | int start = mBuilder.length(); 970 | mBuilder.append(""); 971 | int end = start + 5; 972 | if (imageBitmap != null) { 973 | mBuilder.setSpan(new CustomImageSpan(imageBitmap, alignImage), start, end, flag); 974 | } else if (imageDrawable != null) { 975 | mBuilder.setSpan(new CustomImageSpan(imageDrawable, alignImage), start, end, flag); 976 | } else if (imageUri != null) { 977 | mBuilder.setSpan(new CustomImageSpan(imageUri, alignImage), start, end, flag); 978 | } else if (imageResourceId != -1) { 979 | mBuilder.setSpan(new CustomImageSpan(imageResourceId, alignImage), start, end, flag); 980 | } 981 | } 982 | 983 | private void updateSpace() { 984 | int start = mBuilder.length(); 985 | mBuilder.append("< >"); 986 | int end = start + 3; 987 | mBuilder.setSpan(new SpaceSpan(spaceSize, spaceColor), start, end, flag); 988 | } 989 | 990 | class CustomLineHeightSpan extends CharacterStyle 991 | implements LineHeightSpan { 992 | 993 | private final int height; 994 | 995 | static final int ALIGN_CENTER = 2; 996 | 997 | static final int ALIGN_TOP = 3; 998 | 999 | final int mVerticalAlignment; 1000 | 1001 | CustomLineHeightSpan(int height, int verticalAlignment) { 1002 | this.height = height; 1003 | mVerticalAlignment = verticalAlignment; 1004 | } 1005 | 1006 | @Override 1007 | public void chooseHeight(final CharSequence text, final int start, final int end, 1008 | final int spanstartv, final int v, final Paint.FontMetricsInt fm) { 1009 | int need = height - (v + fm.descent - fm.ascent - spanstartv); 1010 | // if (need > 0) { 1011 | if (mVerticalAlignment == ALIGN_TOP) { 1012 | fm.descent += need; 1013 | } else if (mVerticalAlignment == ALIGN_CENTER) { 1014 | fm.descent += need / 2; 1015 | fm.ascent -= need / 2; 1016 | } else { 1017 | fm.ascent -= need; 1018 | } 1019 | // } 1020 | need = height - (v + fm.bottom - fm.top - spanstartv); 1021 | // if (need > 0) { 1022 | if (mVerticalAlignment == ALIGN_TOP) { 1023 | fm.top += need; 1024 | } else if (mVerticalAlignment == ALIGN_CENTER) { 1025 | fm.bottom += need / 2; 1026 | fm.top -= need / 2; 1027 | } else { 1028 | fm.top -= need; 1029 | } 1030 | // } 1031 | } 1032 | 1033 | @Override 1034 | public void updateDrawState(final TextPaint tp) { 1035 | 1036 | } 1037 | } 1038 | 1039 | class SpaceSpan extends ReplacementSpan { 1040 | 1041 | private final int width; 1042 | private final int color; 1043 | 1044 | private SpaceSpan(final int width) { 1045 | this(width, Color.TRANSPARENT); 1046 | } 1047 | 1048 | private SpaceSpan(final int width, final int color) { 1049 | super(); 1050 | this.width = width; 1051 | this.color = color; 1052 | } 1053 | 1054 | @Override 1055 | public int getSize(@NonNull final Paint paint, final CharSequence text, 1056 | @IntRange(from = 0) final int start, 1057 | @IntRange(from = 0) final int end, 1058 | @Nullable final Paint.FontMetricsInt fm) { 1059 | return width; 1060 | } 1061 | 1062 | @Override 1063 | public void draw(@NonNull final Canvas canvas, final CharSequence text, 1064 | @IntRange(from = 0) final int start, 1065 | @IntRange(from = 0) final int end, 1066 | final float x, final int top, final int y, final int bottom, 1067 | @NonNull final Paint paint) { 1068 | Paint.Style style = paint.getStyle(); 1069 | int color = paint.getColor(); 1070 | 1071 | paint.setStyle(Paint.Style.FILL); 1072 | paint.setColor(this.color); 1073 | 1074 | canvas.drawRect(x, top, x + width, bottom, paint); 1075 | 1076 | paint.setStyle(style); 1077 | paint.setColor(color); 1078 | } 1079 | } 1080 | 1081 | class CustomQuoteSpan implements LeadingMarginSpan { 1082 | 1083 | private final int color; 1084 | private final int stripeWidth; 1085 | private final int gapWidth; 1086 | 1087 | private CustomQuoteSpan(final int color, final int stripeWidth, final int gapWidth) { 1088 | super(); 1089 | this.color = color; 1090 | this.stripeWidth = stripeWidth; 1091 | this.gapWidth = gapWidth; 1092 | } 1093 | 1094 | public int getLeadingMargin(final boolean first) { 1095 | return stripeWidth + gapWidth; 1096 | } 1097 | 1098 | public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir, 1099 | final int top, final int baseline, final int bottom, 1100 | final CharSequence text, final int start, final int end, 1101 | final boolean first, final Layout layout) { 1102 | Paint.Style style = p.getStyle(); 1103 | int color = p.getColor(); 1104 | 1105 | p.setStyle(Paint.Style.FILL); 1106 | p.setColor(this.color); 1107 | 1108 | c.drawRect(x, top, x + dir * stripeWidth, bottom, p); 1109 | 1110 | p.setStyle(style); 1111 | p.setColor(color); 1112 | } 1113 | } 1114 | 1115 | class CustomBulletSpan implements LeadingMarginSpan { 1116 | 1117 | private final int color; 1118 | private final int radius; 1119 | private final int gapWidth; 1120 | 1121 | private Path sBulletPath = null; 1122 | 1123 | private CustomBulletSpan(final int color, final int radius, final int gapWidth) { 1124 | this.color = color; 1125 | this.radius = radius; 1126 | this.gapWidth = gapWidth; 1127 | } 1128 | 1129 | public int getLeadingMargin(final boolean first) { 1130 | return 2 * radius + gapWidth; 1131 | } 1132 | 1133 | public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir, 1134 | final int top, final int baseline, final int bottom, 1135 | final CharSequence text, final int start, final int end, 1136 | final boolean first, final Layout l) { 1137 | if (((Spanned) text).getSpanStart(this) == start) { 1138 | Paint.Style style = p.getStyle(); 1139 | int oldColor = 0; 1140 | oldColor = p.getColor(); 1141 | p.setColor(color); 1142 | p.setStyle(Paint.Style.FILL); 1143 | if (c.isHardwareAccelerated()) { 1144 | if (sBulletPath == null) { 1145 | sBulletPath = new Path(); 1146 | // Bullet is slightly better to avoid aliasing artifacts on mdpi devices. 1147 | sBulletPath.addCircle(0.0f, 0.0f, radius, Path.Direction.CW); 1148 | } 1149 | c.save(); 1150 | c.translate(x + dir * radius, (top + bottom) / 2.0f); 1151 | c.drawPath(sBulletPath, p); 1152 | c.restore(); 1153 | } else { 1154 | c.drawCircle(x + dir * radius, (top + bottom) / 2.0f, radius, p); 1155 | } 1156 | p.setColor(oldColor); 1157 | p.setStyle(style); 1158 | } 1159 | } 1160 | } 1161 | 1162 | @SuppressLint("ParcelCreator") 1163 | class CustomTypefaceSpan extends TypefaceSpan { 1164 | 1165 | private final Typeface newType; 1166 | 1167 | private CustomTypefaceSpan(final Typeface type) { 1168 | super(""); 1169 | newType = type; 1170 | } 1171 | 1172 | @Override 1173 | public void updateDrawState(final TextPaint textPaint) { 1174 | apply(textPaint, newType); 1175 | } 1176 | 1177 | @Override 1178 | public void updateMeasureState(final TextPaint paint) { 1179 | apply(paint, newType); 1180 | } 1181 | 1182 | private void apply(final Paint paint, final Typeface tf) { 1183 | int oldStyle; 1184 | Typeface old = paint.getTypeface(); 1185 | if (old == null) { 1186 | oldStyle = 0; 1187 | } else { 1188 | oldStyle = old.getStyle(); 1189 | } 1190 | 1191 | int fake = oldStyle & ~tf.getStyle(); 1192 | if ((fake & Typeface.BOLD) != 0) { 1193 | paint.setFakeBoldText(true); 1194 | } 1195 | 1196 | if ((fake & Typeface.ITALIC) != 0) { 1197 | paint.setTextSkewX(-0.25f); 1198 | } 1199 | 1200 | paint.getShader(); 1201 | 1202 | paint.setTypeface(tf); 1203 | } 1204 | } 1205 | 1206 | class CustomImageSpan extends CustomDynamicDrawableSpan { 1207 | private Drawable mDrawable; 1208 | private Uri mContentUri; 1209 | private int mResourceId; 1210 | 1211 | private CustomImageSpan(final Bitmap b, final int verticalAlignment) { 1212 | super(verticalAlignment); 1213 | mDrawable = new BitmapDrawable(mContext.getResources(), b); 1214 | mDrawable.setBounds( 1215 | 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight() 1216 | ); 1217 | } 1218 | 1219 | private CustomImageSpan(final Drawable d, final int verticalAlignment) { 1220 | super(verticalAlignment); 1221 | mDrawable = d; 1222 | mDrawable.setBounds( 1223 | 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight() 1224 | ); 1225 | } 1226 | 1227 | private CustomImageSpan(final Uri uri, final int verticalAlignment) { 1228 | super(verticalAlignment); 1229 | mContentUri = uri; 1230 | } 1231 | 1232 | private CustomImageSpan(@DrawableRes final int resourceId, final int verticalAlignment) { 1233 | super(verticalAlignment); 1234 | mResourceId = resourceId; 1235 | } 1236 | 1237 | @Override 1238 | public Drawable getDrawable() { 1239 | Drawable drawable = null; 1240 | if (mDrawable != null) { 1241 | drawable = mDrawable; 1242 | } else if (mContentUri != null) { 1243 | Bitmap bitmap; 1244 | try { 1245 | InputStream is = 1246 | mContext.getContentResolver().openInputStream(mContentUri); 1247 | bitmap = BitmapFactory.decodeStream(is); 1248 | drawable = new BitmapDrawable(mContext.getResources(), bitmap); 1249 | drawable.setBounds( 1250 | 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() 1251 | ); 1252 | if (is != null) { 1253 | is.close(); 1254 | } 1255 | } catch (Exception e) { 1256 | Log.e("sms", "Failed to loaded content " + mContentUri, e); 1257 | } 1258 | } else { 1259 | try { 1260 | drawable = ContextCompat.getDrawable(mContext, mResourceId); 1261 | drawable.setBounds( 1262 | 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() 1263 | ); 1264 | } catch (Exception e) { 1265 | Log.e("sms", "Unable to find resource: " + mResourceId); 1266 | } 1267 | } 1268 | return drawable; 1269 | } 1270 | } 1271 | 1272 | abstract class CustomDynamicDrawableSpan extends ReplacementSpan { 1273 | 1274 | static final int ALIGN_BOTTOM = 0; 1275 | 1276 | static final int ALIGN_BASELINE = 1; 1277 | 1278 | static final int ALIGN_CENTER = 2; 1279 | 1280 | static final int ALIGN_TOP = 3; 1281 | 1282 | final int mVerticalAlignment; 1283 | 1284 | private CustomDynamicDrawableSpan() { 1285 | mVerticalAlignment = ALIGN_BOTTOM; 1286 | } 1287 | 1288 | private CustomDynamicDrawableSpan(final int verticalAlignment) { 1289 | mVerticalAlignment = verticalAlignment; 1290 | } 1291 | 1292 | public abstract Drawable getDrawable(); 1293 | 1294 | @Override 1295 | public int getSize(@NonNull final Paint paint, final CharSequence text, 1296 | final int start, final int end, final Paint.FontMetricsInt fm) { 1297 | Drawable d = getCachedDrawable(); 1298 | Rect rect = d.getBounds(); 1299 | if (fm != null) { 1300 | // LogUtils.d("fm.top: " + fm.top, 1301 | // "fm.ascent: " + fm.ascent, 1302 | // "fm.descent: " + fm.descent, 1303 | // "fm.bottom: " + fm.bottom, 1304 | // "lineHeight: " + (fm.bottom - fm.top)); 1305 | int lineHeight = fm.bottom - fm.top; 1306 | if (lineHeight < rect.height()) { 1307 | if (mVerticalAlignment == ALIGN_TOP) { 1308 | fm.top = fm.top; 1309 | fm.bottom = rect.height() + fm.top; 1310 | } else if (mVerticalAlignment == ALIGN_CENTER) { 1311 | fm.top = -rect.height() / 2 - lineHeight / 4; 1312 | fm.bottom = rect.height() / 2 - lineHeight / 4; 1313 | } else { 1314 | fm.top = -rect.height() + fm.bottom; 1315 | fm.bottom = fm.bottom; 1316 | } 1317 | fm.ascent = fm.top; 1318 | fm.descent = fm.bottom; 1319 | } 1320 | } 1321 | return rect.right; 1322 | } 1323 | 1324 | @Override 1325 | public void draw(@NonNull final Canvas canvas, final CharSequence text, 1326 | final int start, final int end, final float x, 1327 | final int top, final int y, final int bottom, @NonNull final Paint paint) { 1328 | Drawable d = getCachedDrawable(); 1329 | Rect rect = d.getBounds(); 1330 | canvas.save(); 1331 | float transY; 1332 | int lineHeight = bottom - top; 1333 | // LogUtils.d("rectHeight: " + rect.height(), 1334 | // "lineHeight: " + (bottom - top)); 1335 | if (rect.height() < lineHeight) { 1336 | if (mVerticalAlignment == ALIGN_TOP) { 1337 | transY = top; 1338 | } else if (mVerticalAlignment == ALIGN_CENTER) { 1339 | transY = (bottom + top - rect.height()) / 2; 1340 | } else if (mVerticalAlignment == ALIGN_BASELINE) { 1341 | transY = y - rect.height(); 1342 | } else { 1343 | transY = bottom - rect.height(); 1344 | } 1345 | canvas.translate(x, transY); 1346 | } else { 1347 | canvas.translate(x, top); 1348 | } 1349 | d.draw(canvas); 1350 | canvas.restore(); 1351 | } 1352 | 1353 | private Drawable getCachedDrawable() { 1354 | WeakReference wr = mDrawableRef; 1355 | Drawable d = null; 1356 | if (wr != null) { 1357 | d = wr.get(); 1358 | } 1359 | if (d == null) { 1360 | d = getDrawable(); 1361 | mDrawableRef = new WeakReference<>(d); 1362 | } 1363 | return d; 1364 | } 1365 | 1366 | private WeakReference mDrawableRef; 1367 | } 1368 | 1369 | class ShaderSpan extends CharacterStyle implements UpdateAppearance { 1370 | private Shader mShader; 1371 | 1372 | private ShaderSpan(final Shader shader) { 1373 | this.mShader = shader; 1374 | } 1375 | 1376 | @Override 1377 | public void updateDrawState(final TextPaint tp) { 1378 | tp.setShader(mShader); 1379 | } 1380 | } 1381 | 1382 | class ShadowSpan extends CharacterStyle implements UpdateAppearance { 1383 | private float radius; 1384 | private float dx, dy; 1385 | private int shadowColor; 1386 | 1387 | private ShadowSpan(final float radius, 1388 | final float dx, 1389 | final float dy, 1390 | final int shadowColor) { 1391 | this.radius = radius; 1392 | this.dx = dx; 1393 | this.dy = dy; 1394 | this.shadowColor = shadowColor; 1395 | } 1396 | 1397 | @Override 1398 | public void updateDrawState(final TextPaint tp) { 1399 | tp.setShadowLayer(radius, dx, dy, shadowColor); 1400 | } 1401 | } 1402 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_btn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_custom_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/bg_custom_update.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_custom_update_dialog.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/bg_custom_update_dialog.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/ic_close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/ic_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/ic_update.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_java_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 |