Repository: teprinciple/UpdateAppDemo Branch: master Commit: 5afcb34f0d4a Files: 75 Total size: 148.8 KB Directory structure: gitextract_os787tpu/ ├── .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 │ ├── layout/ │ │ ├── activity_java_demo.xml │ │ ├── activity_main.xml │ │ ├── check_md5_demo_activity.xml │ │ └── view_update_dialog_custom.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── values-w820dp/ │ └── dimens.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── readme/ │ ├── README_1.5.2.md │ ├── version.md │ └── 自定义UI.md ├── settings.gradle ├── 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 ├── layout/ │ ├── view_update_dialog_plentiful.xml │ └── view_update_dialog_simple.xml ├── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── values-en/ │ └── strings.xml └── xml/ ├── network_security_config.xml └── update_file_paths.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/android.yml ================================================ # workflow的名称,会显示在github 的项目的Actions的右边列表中,如下图 name: Android CI # 在满足以下条件触发这个workflow on: push: # 在指定的远程分支 master上,发生推送时 branches: [ master ] jobs: # 多个job,如果有多个,每个以“-”开头,这里只有一个 job build: runs-on: ubuntu-latest # 该job 运行的系统环境,支持ubuntu 、windows、macOS # 下面是多个step ,每个以“-”开头 steps: - uses: actions/checkout@v2 # step:检查分支 - name: set up JDK 1.8 # step:设置jdk版本 uses: actions/setup-java@v1 # 引用公共action with: java-version: 1.8 - name: Build with Gradle # step:打包apk # 运行打包命令 run: chmod +x gradlew &&./gradlew assembleRelease #step:上传apk 到action,在右上角查看 # 官方文档 https://help.github.com/cn/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts#uploading-build-and-test-artifacts - name: Upload APK uses: actions/upload-artifact@v1 with: name: app path: app/build/outputs/apk/release/app-release.apk ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: README.md ================================================ # UpdateAppUtils2.0 [ ![](https://img.shields.io/badge/platform-android-green.svg) ](http://developer.android.com/index.html) [ ![Download](https://api.bintray.com/packages/teprinciple/maven/updateapputils/images/download.svg) ](https://bintray.com/teprinciple/maven/updateapputils/_latestVersion) ### 一行代码,快速实现app在线下载更新 A simple library for Android update app #### UpdateAppUtils2.0 特点 * Kotlin First,Kotlin开发 * 支持AndroidX * 支持Md5签名验证 * 支持自定义任意UI * 适配中英文 * 适配至Android 10 * 通知栏图片自定义 * 支持修改是否每次显示弹窗(非强更) * 支持安装完成后自动删除安装包 UpdateAppUtils2.0功能结构变化巨大,建议使用2.0以上版本;[2.0以前版本文档](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/README_1.5.2.md) #### 效果图 ### 集成 ``` repositories { jcenter() } ``` Support ``` implementation 'com.teprinciple:updateapputils:2.3.0' ``` AndroidX项目 ``` 注意,由于操作失误bintray 中updateapputilsX被我删掉, 所以2.3.0以后使用updateapputilsx。之前的仍使用updateapputilsX //implementation 'com.teprinciple:updateapputilsX:2.2.1' implementation 'com.teprinciple:updateapputilsx:2.3.0' ``` ### 使用 下面为kotlin使用示例,Java示例请参考[JavaDemo](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java) #### 1、快速使用 ##### 注意:部分手机SDK内部初始化不了context,造成context空指针,建议在application或者使用SDK前先初始化 ``` UpdateAppUtils.init(context) ``` ``` UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .update() ``` #### 2、多配置使用 ``` // ui配置 val uiConfig = UiConfig().apply { uiType = UiType.PLENTIFUL cancelBtnText = "下次再说" updateLogoImgRes = R.drawable.ic_update updateBtnBgRes = R.drawable.bg_btn titleTextColor = Color.BLACK titleTextSize = 18f contentTextColor = Color.parseColor("#88e16531") } // 更新配置 val updateConfig = UpdateConfig().apply { force = true checkWifi = true needCheckMd5 = true isShowNotification = true notifyImgRes = R.drawable.ic_logo apkSavePath = Environment.getExternalStorageDirectory().absolutePath +"/teprinciple" apkSaveName = "teprinciple" } UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .updateConfig(updateConfig) .uiConfig(uiConfig) .setUpdateDownloadListener(object : UpdateDownloadListener{ // do something }) .update() ``` #### 3、md5校验说明 为了保证app的安全性,避免apk被二次打包的风险。UpdateAppUtils内部提供了对签名文件md5值校验的接口; 首先你需要保证当前应用和服务器apk使用同一个签名文件进行了签名(一定要保管好自己的签名文件,否则检验就失去了意义), 然后你需要将UpdateConfig 的 needCheckMd5 设置为true,并在Md5CheckResultListener监听中,监听校验返回结果。具体使用可参考 [CheckMd5DemoActivity](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt) ``` UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .updateConfig(updateConfig) // needCheckMd5设置为true .setMd5CheckResultListener(object : Md5CheckResultListener{ override fun onResult(result: Boolean) { // true:检验通过,false:检验失败 } }) ``` #### 4、自定义UI UpdateAppUtils内置了两套UI,你可以通过修改[UiConfig](#UiConfig)进行UI内容的自定义; 当然当内部UI模板与你期望UI差别很大时,你可以采用[完全自定义UI](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/%E8%87%AA%E5%AE%9A%E4%B9%89UI.md) ### Api说明 #### 1、UpdateAppUtils Api | api | 说明 | 默认值 | 必须设置 | |:-------------- |:------------------------------------ |:--------------------- |:------ | | fun apkUrl(apkUrl: String)| 更新包服务器url | null | true | | fun update() | UpdateAppUtils入口 | - | true | | fun updateTitle(title: String) | 更新标题 | 版本更新啦! | false | | fun updateContent(content: String) | 更新内容 | 发现新版本,立即更新 | false | | fun updateConfig(config: UpdateConfig) | 更新配置 | 查看源码 | false | | fun uiConfig(uiConfig: UiConfig) | 更新弹窗UI配置 | 查看源码 | false | | fun setUpdateDownloadListener() | 下载过程监听 | null | false | | fun setMd5CheckResultListener() | md5校验结果回调 | null | false | | fun setOnInitUiListener() | 初始化更新弹窗UI回调 | null | false | | fun deleteInstalledApk() | 删除已安装的apk | - | false | | fun setCancelBtnClickListener() | 暂不更新按钮点击监听 | - | false | | fun setUpdateBtnClickListener() | 立即更新按钮点击监听 | - | false | #### 2、UpdateConfig:更新配置说明 | 属性 | 说明 | 默认值 | |:--------------------- |:------------------------------------ |:------ | | isDebug | 是否输出【UpdateAppUtils】为Tag的日志| true | | force | 是否强制更新,强制时无取消按钮 | false | | apkSavePath | apk下载存放位置 | 包名目录 | | apkSaveName | apk保存文件名 | 项目名 | | downloadBy | 下载方式 | DownLoadBy.APP | | needCheckMd5 | 是否需要校验apk签名md5 | false | | checkWifi | 检查是否wifi | false | | isShowNotification | 是否显示通知栏进度 | true | | notifyImgRes | 通知栏图标 | 项目icon | | serverVersionName | 服务器上apk版本名 | 无 | | serverVersionCode | 服务器上apk版本号 | 无 | | alwaysShow | 是否每次显示更新弹窗(非强更) | true | | thisTimeShow | 本次是否显示更新弹窗(非强更) | false | | alwaysShowDownLoadDialog| 是否需要显示更新下载进度弹窗(非强更) | false | | showDownloadingToast | 开始下载时是否显示Toast | true | #### 3、UiConfig:更新弹窗Ui配置说明
| 属性 | 说明 | 默认值 | |:--------------------- |:------------------------------------ |:------ | | uiType | ui模板 | UiType.SIMPLE | | customLayoutId | 自定义布局id | false | | updateLogoImgRes | 更新弹窗logo图片资源id | - | | titleTextSize | 标题字体大小 | 16sp | | titleTextColor | 标题字体颜色 | - | | contentTextSize | 内容字体大小 | 14sp | | contentTextColor | 内容字体颜色 | - | | updateBtnBgColor | 更新按钮背景颜色 | - | | updateBtnBgRes | 更新按钮背景资源id | - | | updateBtnTextColor | 更新按钮字体颜色 | - | | updateBtnTextSize | 更新按钮文字大小 | 14sp | | updateBtnText | 更新按钮文案 | 立即更新 | | cancelBtnBgColor | 取消按钮背景颜色 | - | | cancelBtnBgRes | 取消按钮背景资源id | - | | cancelBtnTextColor | 取消按钮文字颜色 | - | | cancelBtnTextSize | 取消按钮文字大小 | 14sp | | cancelBtnText | 取消按钮文案 | 暂不更新 | | downloadingToastText | 开始下载时的Toast提示文字 | 更新下载中... | | downloadingBtnText | 下载中 下载按钮以及通知栏标题前缀,进度自动拼接在后面 | 下载中 | | downloadFailText | 下载出错时,下载按钮及通知栏标题 | 下载出错,点击重试 | ### Demo体验 ### 更新日志 #### 2.3.0 * 修复部分手机context空指针异常 ##### [更多历史版本](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/version.md) ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "com.example.teprinciple.updateappdemo" minSdkVersion 19 targetSdkVersion 29 versionCode 203 versionName "2.0.3" } signingConfigs { config { storeFile file('../update.jks') storePassword '123456' keyAlias 'update' keyPassword '123456' } } buildTypes { release { minifyEnabled false signingConfig signingConfigs.debug proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { minifyEnabled false signingConfig signingConfigs.config } } lintOptions { abortOnError false } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation project(':updateapputils') implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Users\Teprinciple\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/androidTest/java/com/example/teprinciple/updateappdemo/ExampleInstrumentedTest.java ================================================ package com.example.teprinciple.updateappdemo; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumentation test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.example.teprinciple.updateappdemo", appContext.getPackageName()); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt ================================================ package com.example.teprinciple.updateappdemo import android.os.Bundle import android.os.Environment import android.support.v7.app.AppCompatActivity import android.widget.Toast import kotlinx.android.synthetic.main.check_md5_demo_activity.* import listener.Md5CheckResultListener import model.UpdateConfig import update.UpdateAppUtils /** * desc: md5校验示例 * time: 2019/7/1 * @author yk */ class CheckMd5DemoActivity : AppCompatActivity() { /** * 已签名的apk */ private val signedApkUrl = "http://118.24.148.250:8080/yk/update_signed.apk" /** * 非正规签名的apk */ private val notSignedApkUrl = "http://118.24.148.250:8080/yk/update_not_signed.apk" private val updateTitle = "发现新版本V2.0.0" private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.check_md5_demo_activity) // 更新配置 val updateConfig = UpdateConfig().apply { force = true needCheckMd5 = true } // 正确签名 btn_signed.setOnClickListener { updateConfig.apply { apkSaveName = "signed" } UpdateAppUtils .getInstance() .apkUrl(signedApkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .updateConfig(updateConfig) .setMd5CheckResultListener(object : Md5CheckResultListener { override fun onResult(result: Boolean) { Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show() } }) .update() } // 错误签名 btn_not_signed.setOnClickListener { updateConfig.apply { apkSaveName = "not_signed" } UpdateAppUtils .getInstance() .apkUrl(notSignedApkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .updateConfig(updateConfig) .setMd5CheckResultListener(object : Md5CheckResultListener { override fun onResult(result: Boolean) { Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show() } }) .update() } } } ================================================ FILE: app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java ================================================ package com.example.teprinciple.updateappdemo; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.View; import org.jetbrains.annotations.NotNull; import constant.UiType; import listener.Md5CheckResultListener; import listener.UpdateDownloadListener; import model.UiConfig; import model.UpdateConfig; import update.UpdateAppUtils; /** * desc: java使用实例 * time: 2019/6/27 * @author yk */ public class JavaDemoActivity extends AppCompatActivity { private String apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk"; private String updateTitle = "发现新版本V2.0.0"; private String updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_java_demo); UpdateAppUtils.init(this); findViewById(R.id.btn_java).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { UpdateConfig updateConfig = new UpdateConfig(); updateConfig.setCheckWifi(true); updateConfig.setNeedCheckMd5(true); updateConfig.setNotifyImgRes(R.drawable.ic_logo); UiConfig uiConfig = new UiConfig(); uiConfig.setUiType(UiType.PLENTIFUL); UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .uiConfig(uiConfig) .updateConfig(updateConfig) .setMd5CheckResultListener(new Md5CheckResultListener() { @Override public void onResult(boolean result) { } }) .setUpdateDownloadListener(new UpdateDownloadListener() { @Override public void onStart() { } @Override public void onDownload(int progress) { } @Override public void onFinish() { } @Override public void onError(@NotNull Throwable e) { } }) .update(); } }); } } ================================================ FILE: app/src/main/java/com/example/teprinciple/updateappdemo/MainActivity.kt ================================================ package com.example.teprinciple.updateappdemo import android.content.Intent import android.graphics.Color import android.os.Bundle import android.os.Environment import android.support.v7.app.AppCompatActivity import android.view.View import android.widget.TextView import android.widget.Toast import constant.DownLoadBy import constant.UiType import kotlinx.android.synthetic.main.activity_main.* import listener.OnBtnClickListener import listener.OnInitUiListener import listener.UpdateDownloadListener import model.UiConfig import model.UpdateConfig import update.UpdateAppUtils class MainActivity : AppCompatActivity() { private val apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk" // private val apkUrl = "https://github.com/AlexLiuSheng/CheckVersionLib/blob/master/library/src/main/java/com/allenliu/versionchecklib/utils/AppUtils.java" private val updateTitle = "发现新版本V2.0.0" private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) UpdateAppUtils.init(this) // 基本使用 btn_basic_use.setOnClickListener { UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateConfig(UpdateConfig(apkSaveName = "up_1.1")) .uiConfig(UiConfig(uiType = UiType.SIMPLE)) .updateContent(updateContent) .update() } // 浏览器下载 btn_download_by_browser.setOnClickListener { // 使用SpannableString val content = SpanUtils(this) .appendLine("1、Kotlin重构版") .appendLine("2、支持自定义UI").setForegroundColor(Color.RED) .appendLine("3、增加md5校验").setForegroundColor(Color.parseColor("#88e16531")).setFontSize(20, true) .appendLine("4、更多功能等你探索").setBoldItalic() .appendLine().appendImage(R.mipmap.ic_launcher).setBoldItalic() .create() UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateContent(content) .updateConfig(UpdateConfig().apply { downloadBy = DownLoadBy.BROWSER // alwaysShow = false serverVersionName = "2.0.0" }) .uiConfig(UiConfig(uiType = UiType.PLENTIFUL)) // 设置 取消 按钮点击事件 .setCancelBtnClickListener(object : OnBtnClickListener { override fun onClick(): Boolean { Toast.makeText(this@MainActivity, "cancel btn click", Toast.LENGTH_SHORT).show() return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑 } }) // 设置 立即更新 按钮点击事件 .setUpdateBtnClickListener(object : OnBtnClickListener { override fun onClick(): Boolean { Toast.makeText(this@MainActivity, "update btn click", Toast.LENGTH_SHORT).show() return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑 } }) .update() } // 自定义UI btn_custom_ui.setOnClickListener { UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .updateConfig(UpdateConfig(alwaysShowDownLoadDialog = true)) .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom)) .setOnInitUiListener(object : OnInitUiListener { override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) { view?.findViewById(R.id.tv_update_title)?.text = "版本更新啦" view?.findViewById(R.id.tv_version_name)?.text = "V2.0.0" // do more... } }) .update() } // java使用示例 btn_java_sample.setOnClickListener { startActivity(Intent(this, JavaDemoActivity::class.java)) } // md5校验 btn_check_md5.setOnClickListener { startActivity(Intent(this, CheckMd5DemoActivity::class.java)) } // 高级使用 btn_higher_level_use.setOnClickListener { // ui配置 val uiConfig = UiConfig().apply { uiType = UiType.PLENTIFUL cancelBtnText = "下次再说" updateLogoImgRes = R.drawable.ic_update updateBtnBgRes = R.drawable.bg_btn titleTextColor = Color.BLACK titleTextSize = 18f contentTextColor = Color.parseColor("#88e16531") } // 更新配置 val updateConfig = UpdateConfig().apply { force = true isDebug = true checkWifi = true isShowNotification = true notifyImgRes = R.drawable.ic_logo apkSavePath = Environment.getExternalStorageDirectory().absolutePath + "/teprinciple" apkSaveName = "teprinciple" } UpdateAppUtils .getInstance() .apkUrl(apkUrl) .updateTitle(updateTitle) .updateContent(updateContent) .updateConfig(updateConfig) .uiConfig(uiConfig) .setUpdateDownloadListener(object : UpdateDownloadListener { override fun onStart() { } override fun onDownload(progress: Int) { } override fun onFinish() { } override fun onError(e: Throwable) { } }) .update() } } } ================================================ FILE: app/src/main/java/com/example/teprinciple/updateappdemo/SpanUtils.java ================================================ package com.example.teprinciple.updateappdemo; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.FloatRange; import android.support.annotation.IntDef; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.text.Layout; import android.text.Layout.Alignment; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.LeadingMarginSpan; import android.text.style.LineHeightSpan; import android.text.style.MaskFilterSpan; import android.text.style.RelativeSizeSpan; import android.text.style.ReplacementSpan; import android.text.style.ScaleXSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; import android.text.style.SuperscriptSpan; import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.text.style.UpdateAppearance; import android.util.Log; import java.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import static android.graphics.BlurMaskFilter.Blur; /** *
 *     author: Blankj
 *     blog  : http://blankj.com
 *     usage : https://www.jianshu.com/p/509b0d2626f4
 *     time  : 16/12/13
 *     desc  : utils about span
 * 
*/ public final class SpanUtils { private static final int COLOR_DEFAULT = 0xFEFFFFFF; public static final int ALIGN_BOTTOM = 0; public static final int ALIGN_BASELINE = 1; public static final int ALIGN_CENTER = 2; public static final int ALIGN_TOP = 3; @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP}) @Retention(RetentionPolicy.SOURCE) public @interface Align { } private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private CharSequence mText; private int flag; private int foregroundColor; private int backgroundColor; private int lineHeight; private int alignLine; private int quoteColor; private int stripeWidth; private int quoteGapWidth; private int first; private int rest; private int bulletColor; private int bulletRadius; private int bulletGapWidth; private int fontSize; private boolean fontSizeIsDp; private float proportion; private float xProportion; private boolean isStrikethrough; private boolean isUnderline; private boolean isSuperscript; private boolean isSubscript; private boolean isBold; private boolean isItalic; private boolean isBoldItalic; private String fontFamily; private Typeface typeface; private Alignment alignment; private ClickableSpan clickSpan; private String url; private float blurRadius; private Blur style; private Shader shader; private float shadowRadius; private float shadowDx; private float shadowDy; private int shadowColor; private Object[] spans; private Bitmap imageBitmap; private Drawable imageDrawable; private Uri imageUri; private int imageResourceId; private int alignImage; private int spaceSize; private int spaceColor; private SpannableStringBuilder mBuilder; private int mType; private final int mTypeCharSequence = 0; private final int mTypeImage = 1; private final int mTypeSpace = 2; private Context mContext; public SpanUtils(Context context) { mContext = context.getApplicationContext(); mBuilder = new SpannableStringBuilder(); mText = ""; setDefault(); } private void setDefault() { flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; foregroundColor = COLOR_DEFAULT; backgroundColor = COLOR_DEFAULT; lineHeight = -1; quoteColor = COLOR_DEFAULT; first = -1; bulletColor = COLOR_DEFAULT; fontSize = -1; proportion = -1; xProportion = -1; isStrikethrough = false; isUnderline = false; isSuperscript = false; isSubscript = false; isBold = false; isItalic = false; isBoldItalic = false; fontFamily = null; typeface = null; alignment = null; clickSpan = null; url = null; blurRadius = -1; shader = null; shadowRadius = -1; spans = null; imageBitmap = null; imageDrawable = null; imageUri = null; imageResourceId = -1; spaceSize = -1; } /** * Set the span of flag. * * @param flag * The flag. *
    *
  • {@link Spanned#SPAN_INCLUSIVE_EXCLUSIVE}
  • *
  • {@link Spanned#SPAN_INCLUSIVE_INCLUSIVE}
  • *
  • {@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE}
  • *
  • {@link Spanned#SPAN_EXCLUSIVE_INCLUSIVE}
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils setFlag(final int flag) { this.flag = flag; return this; } /** * Set the span of foreground's color. * * @param color * The color of foreground * @return the single {@link SpanUtils} instance */ public SpanUtils setForegroundColor(@ColorInt final int color) { this.foregroundColor = color; return this; } /** * Set the span of background's color. * * @param color * The color of background * @return the single {@link SpanUtils} instance */ public SpanUtils setBackgroundColor(@ColorInt final int color) { this.backgroundColor = color; return this; } /** * Set the span of line height. * * @param lineHeight * The line height, in pixel. * @return the single {@link SpanUtils} instance */ public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight) { return setLineHeight(lineHeight, ALIGN_CENTER); } /** * Set the span of line height. * * @param lineHeight * The line height, in pixel. * @param align * The alignment. *
    *
  • {@link Align#ALIGN_TOP }
  • *
  • {@link Align#ALIGN_CENTER}
  • *
  • {@link Align#ALIGN_BOTTOM}
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight, @Align final int align) { this.lineHeight = lineHeight; this.alignLine = align; return this; } /** * Set the span of quote's color. * * @param color * The color of quote * @return the single {@link SpanUtils} instance */ public SpanUtils setQuoteColor(@ColorInt final int color) { return setQuoteColor(color, 2, 2); } /** * Set the span of quote's color. * * @param color * The color of quote. * @param stripeWidth * The width of stripe, in pixel. * @param gapWidth * The width of gap, in pixel. * @return the single {@link SpanUtils} instance */ public SpanUtils setQuoteColor(@ColorInt final int color, @IntRange(from = 1) final int stripeWidth, @IntRange(from = 0) final int gapWidth) { this.quoteColor = color; this.stripeWidth = stripeWidth; this.quoteGapWidth = gapWidth; return this; } /** * Set the span of leading margin. * * @param first * The indent for the first line of the paragraph. * @param rest * The indent for the remaining lines of the paragraph. * @return the single {@link SpanUtils} instance */ public SpanUtils setLeadingMargin(@IntRange(from = 0) final int first, @IntRange(from = 0) final int rest) { this.first = first; this.rest = rest; return this; } /** * Set the span of bullet. * * @param gapWidth * The width of gap, in pixel. * @return the single {@link SpanUtils} instance */ public SpanUtils setBullet(@IntRange(from = 0) final int gapWidth) { return setBullet(0, 3, gapWidth); } /** * Set the span of bullet. * * @param color * The color of bullet. * @param radius * The radius of bullet, in pixel. * @param gapWidth * The width of gap, in pixel. * @return the single {@link SpanUtils} instance */ public SpanUtils setBullet(@ColorInt final int color, @IntRange(from = 0) final int radius, @IntRange(from = 0) final int gapWidth) { this.bulletColor = color; this.bulletRadius = radius; this.bulletGapWidth = gapWidth; return this; } /** * Set the span of font's size. * * @param size * The size of font. * @return the single {@link SpanUtils} instance */ public SpanUtils setFontSize(@IntRange(from = 0) final int size) { return setFontSize(size, false); } /** * Set the span of size of font. * * @param size * The size of font. * @param isSp * True to use sp, false to use pixel. * @return the single {@link SpanUtils} instance */ public SpanUtils setFontSize(@IntRange(from = 0) final int size, final boolean isSp) { this.fontSize = size; this.fontSizeIsDp = isSp; return this; } /** * Set the span of proportion of font. * * @param proportion * The proportion of font. * @return the single {@link SpanUtils} instance */ public SpanUtils setFontProportion(final float proportion) { this.proportion = proportion; return this; } /** * Set the span of transverse proportion of font. * * @param proportion * The transverse proportion of font. * @return the single {@link SpanUtils} instance */ public SpanUtils setFontXProportion(final float proportion) { this.xProportion = proportion; return this; } /** * Set the span of strikethrough. * * @return the single {@link SpanUtils} instance */ public SpanUtils setStrikethrough() { this.isStrikethrough = true; return this; } /** * Set the span of underline. * * @return the single {@link SpanUtils} instance */ public SpanUtils setUnderline() { this.isUnderline = true; return this; } /** * Set the span of superscript. * * @return the single {@link SpanUtils} instance */ public SpanUtils setSuperscript() { this.isSuperscript = true; return this; } /** * Set the span of subscript. * * @return the single {@link SpanUtils} instance */ public SpanUtils setSubscript() { this.isSubscript = true; return this; } /** * Set the span of bold. * * @return the single {@link SpanUtils} instance */ public SpanUtils setBold() { isBold = true; return this; } /** * Set the span of bold. * * @return the single {@link SpanUtils} instance */ public SpanUtils setNotBold() { isBold = false; return this; } /** * Set the span of italic. * * @return the single {@link SpanUtils} instance */ public SpanUtils setItalic() { isItalic = true; return this; } /** * Set the span of bold italic. * * @return the single {@link SpanUtils} instance */ public SpanUtils setBoldItalic() { isBoldItalic = true; return this; } /** * Set the span of font family. * * @param fontFamily * The font family. *
    *
  • monospace
  • *
  • serif
  • *
  • sans-serif
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils setFontFamily(@NonNull final String fontFamily) { this.fontFamily = fontFamily; return this; } /** * Set the span of typeface. * * @param typeface * The typeface. * @return the single {@link SpanUtils} instance */ public SpanUtils setTypeface(@NonNull final Typeface typeface) { this.typeface = typeface; return this; } /** * Set the span of alignment. * * @param alignment * The alignment. *
    *
  • {@link Alignment#ALIGN_NORMAL }
  • *
  • {@link Alignment#ALIGN_OPPOSITE}
  • *
  • {@link Alignment#ALIGN_CENTER }
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils setAlign(@NonNull final Alignment alignment) { this.alignment = alignment; return this; } /** * Set the span of click. *

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

* * @param clickSpan * The span of click. * @return the single {@link SpanUtils} instance */ public SpanUtils setClickSpan(@NonNull final ClickableSpan clickSpan) { this.clickSpan = clickSpan; return this; } /** * Set the span of url. *

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

* * @param url * The url. * @return the single {@link SpanUtils} instance */ public SpanUtils setUrl(@NonNull final String url) { this.url = url; return this; } /** * Set the span of blur. * * @param radius * The radius of blur. * @param style * The style. *
    *
  • {@link Blur#NORMAL}
  • *
  • {@link Blur#SOLID}
  • *
  • {@link Blur#OUTER}
  • *
  • {@link Blur#INNER}
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils setBlur(@FloatRange(from = 0, fromInclusive = false) final float radius, final Blur style) { this.blurRadius = radius; this.style = style; return this; } /** * Set the span of shader. * * @param shader * The shader. * @return the single {@link SpanUtils} instance */ public SpanUtils setShader(@NonNull final Shader shader) { this.shader = shader; return this; } /** * Set the span of shadow. * * @param radius * The radius of shadow. * @param dx * X-axis offset, in pixel. * @param dy * Y-axis offset, in pixel. * @param shadowColor * The color of shadow. * @return the single {@link SpanUtils} instance */ public SpanUtils setShadow(@FloatRange(from = 0, fromInclusive = false) final float radius, final float dx, final float dy, final int shadowColor) { this.shadowRadius = radius; this.shadowDx = dx; this.shadowDy = dy; this.shadowColor = shadowColor; return this; } /** * Set the spans. * * @param spans * The spans. * @return the single {@link SpanUtils} instance */ public SpanUtils setSpans(@NonNull final Object... spans) { if (spans.length > 0) { this.spans = spans; } return this; } /** * Append the text text. * * @param text * The text. * @return the single {@link SpanUtils} instance */ public SpanUtils append(@NonNull final CharSequence text) { apply(mTypeCharSequence); mText = text; return this; } /** * Append one line. * * @return the single {@link SpanUtils} instance */ public SpanUtils appendLine() { apply(mTypeCharSequence); mText = LINE_SEPARATOR; return this; } /** * Append text and one line. * * @return the single {@link SpanUtils} instance */ public SpanUtils appendLine(@NonNull final CharSequence text) { apply(mTypeCharSequence); mText = text + LINE_SEPARATOR; return this; } /** * Append one image. * * @param bitmap * The bitmap of image. * @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@NonNull final Bitmap bitmap) { return appendImage(bitmap, ALIGN_BOTTOM); } /** * Append one image. * * @param bitmap * The bitmap. * @param align * The alignment. *
    *
  • {@link Align#ALIGN_TOP }
  • *
  • {@link Align#ALIGN_CENTER }
  • *
  • {@link Align#ALIGN_BASELINE}
  • *
  • {@link Align#ALIGN_BOTTOM }
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@NonNull final Bitmap bitmap, @Align final int align) { apply(mTypeImage); this.imageBitmap = bitmap; this.alignImage = align; return this; } /** * Append one image. * * @param drawable * The drawable of image. * @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@NonNull final Drawable drawable) { return appendImage(drawable, ALIGN_BOTTOM); } /** * Append one image. * * @param drawable * The drawable of image. * @param align * The alignment. *
    *
  • {@link Align#ALIGN_TOP }
  • *
  • {@link Align#ALIGN_CENTER }
  • *
  • {@link Align#ALIGN_BASELINE}
  • *
  • {@link Align#ALIGN_BOTTOM }
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@NonNull final Drawable drawable, @Align final int align) { apply(mTypeImage); this.imageDrawable = drawable; this.alignImage = align; return this; } /** * Append one image. * * @param uri * The uri of image. * @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@NonNull final Uri uri) { return appendImage(uri, ALIGN_BOTTOM); } /** * Append one image. * * @param uri * The uri of image. * @param align * The alignment. *
    *
  • {@link Align#ALIGN_TOP }
  • *
  • {@link Align#ALIGN_CENTER }
  • *
  • {@link Align#ALIGN_BASELINE}
  • *
  • {@link Align#ALIGN_BOTTOM }
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@NonNull final Uri uri, @Align final int align) { apply(mTypeImage); this.imageUri = uri; this.alignImage = align; return this; } /** * Append one image. * * @param resourceId * The resource id of image. * @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@DrawableRes final int resourceId) { return appendImage(resourceId, ALIGN_BOTTOM); } /** * Append one image. * * @param resourceId * The resource id of image. * @param align * The alignment. *
    *
  • {@link Align#ALIGN_TOP }
  • *
  • {@link Align#ALIGN_CENTER }
  • *
  • {@link Align#ALIGN_BASELINE}
  • *
  • {@link Align#ALIGN_BOTTOM }
  • *
* @return the single {@link SpanUtils} instance */ public SpanUtils appendImage(@DrawableRes final int resourceId, @Align final int align) { append(Character.toString((char) 0));// it's important for span start with image apply(mTypeImage); this.imageResourceId = resourceId; this.alignImage = align; return this; } /** * Append space. * * @param size * The size of space. * @return the single {@link SpanUtils} instance */ public SpanUtils appendSpace(@IntRange(from = 0) final int size) { return appendSpace(size, Color.TRANSPARENT); } /** * Append space. * * @param size * The size of space. * @param color * The color of space. * @return the single {@link SpanUtils} instance */ public SpanUtils appendSpace(@IntRange(from = 0) final int size, @ColorInt final int color) { apply(mTypeSpace); spaceSize = size; spaceColor = color; return this; } private void apply(final int type) { applyLast(); mType = type; } /** * Create the span string. * * @return the span string */ public SpannableStringBuilder create() { applyLast(); return mBuilder; } private void applyLast() { if (mType == mTypeCharSequence) { updateCharCharSequence(); } else if (mType == mTypeImage) { updateImage(); } else if (mType == mTypeSpace) { updateSpace(); } setDefault(); } private void updateCharCharSequence() { if (mText.length() == 0) return; int start = mBuilder.length(); mBuilder.append(mText); int end = mBuilder.length(); if (foregroundColor != COLOR_DEFAULT) { mBuilder.setSpan(new ForegroundColorSpan(foregroundColor), start, end, flag); } if (backgroundColor != COLOR_DEFAULT) { mBuilder.setSpan(new BackgroundColorSpan(backgroundColor), start, end, flag); } if (first != -1) { mBuilder.setSpan(new LeadingMarginSpan.Standard(first, rest), start, end, flag); } if (quoteColor != COLOR_DEFAULT) { mBuilder.setSpan( new CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth), start, end, flag ); } if (bulletColor != COLOR_DEFAULT) { mBuilder.setSpan( new CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth), start, end, flag ); } // if (imGapWidth != -1) { // if (imBitmap != null) { // mBuilder.setSpan( // new CustomIconMarginSpan(imBitmap, imGapWidth, imAlign), // start, // end, // flag // ); // } else if (imDrawable != null) { // mBuilder.setSpan( // new CustomIconMarginSpan(imDrawable, imGapWidth, imAlign), // start, // end, // flag // ); // } else if (imUri != null) { // mBuilder.setSpan( // new CustomIconMarginSpan(imUri, imGapWidth, imAlign), // start, // end, // flag // ); // } else if (imResourceId != -1) { // mBuilder.setSpan( // new CustomIconMarginSpan(imResourceId, imGapWidth, imAlign), // start, // end, // flag // ); // } // } if (fontSize != -1) { mBuilder.setSpan(new AbsoluteSizeSpan(fontSize, fontSizeIsDp), start, end, flag); } if (proportion != -1) { mBuilder.setSpan(new RelativeSizeSpan(proportion), start, end, flag); } if (xProportion != -1) { mBuilder.setSpan(new ScaleXSpan(xProportion), start, end, flag); } if (lineHeight != -1) { mBuilder.setSpan(new CustomLineHeightSpan(lineHeight, alignLine), start, end, flag); } if (isStrikethrough) { mBuilder.setSpan(new StrikethroughSpan(), start, end, flag); } if (isUnderline) { mBuilder.setSpan(new UnderlineSpan(), start, end, flag); } if (isSuperscript) { mBuilder.setSpan(new SuperscriptSpan(), start, end, flag); } if (isSubscript) { mBuilder.setSpan(new SubscriptSpan(), start, end, flag); } if (isBold) { mBuilder.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag); } if (isItalic) { mBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag); } if (isBoldItalic) { mBuilder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flag); } if (fontFamily != null) { mBuilder.setSpan(new TypefaceSpan(fontFamily), start, end, flag); } if (typeface != null) { mBuilder.setSpan(new CustomTypefaceSpan(typeface), start, end, flag); } if (alignment != null) { mBuilder.setSpan(new AlignmentSpan.Standard(alignment), start, end, flag); } if (clickSpan != null) { mBuilder.setSpan(clickSpan, start, end, flag); } if (url != null) { mBuilder.setSpan(new URLSpan(url), start, end, flag); } if (blurRadius != -1) { mBuilder.setSpan( new MaskFilterSpan(new BlurMaskFilter(blurRadius, style)), start, end, flag ); } if (shader != null) { mBuilder.setSpan(new ShaderSpan(shader), start, end, flag); } if (shadowRadius != -1) { mBuilder.setSpan( new ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor), start, end, flag ); } if (spans != null) { for (Object span : spans) { mBuilder.setSpan(span, start, end, flag); } } } private void updateImage() { int start = mBuilder.length(); mBuilder.append(""); int end = start + 5; if (imageBitmap != null) { mBuilder.setSpan(new CustomImageSpan(imageBitmap, alignImage), start, end, flag); } else if (imageDrawable != null) { mBuilder.setSpan(new CustomImageSpan(imageDrawable, alignImage), start, end, flag); } else if (imageUri != null) { mBuilder.setSpan(new CustomImageSpan(imageUri, alignImage), start, end, flag); } else if (imageResourceId != -1) { mBuilder.setSpan(new CustomImageSpan(imageResourceId, alignImage), start, end, flag); } } private void updateSpace() { int start = mBuilder.length(); mBuilder.append("< >"); int end = start + 3; mBuilder.setSpan(new SpaceSpan(spaceSize, spaceColor), start, end, flag); } class CustomLineHeightSpan extends CharacterStyle implements LineHeightSpan { private final int height; static final int ALIGN_CENTER = 2; static final int ALIGN_TOP = 3; final int mVerticalAlignment; CustomLineHeightSpan(int height, int verticalAlignment) { this.height = height; mVerticalAlignment = verticalAlignment; } @Override public void chooseHeight(final CharSequence text, final int start, final int end, final int spanstartv, final int v, final Paint.FontMetricsInt fm) { int need = height - (v + fm.descent - fm.ascent - spanstartv); // if (need > 0) { if (mVerticalAlignment == ALIGN_TOP) { fm.descent += need; } else if (mVerticalAlignment == ALIGN_CENTER) { fm.descent += need / 2; fm.ascent -= need / 2; } else { fm.ascent -= need; } // } need = height - (v + fm.bottom - fm.top - spanstartv); // if (need > 0) { if (mVerticalAlignment == ALIGN_TOP) { fm.top += need; } else if (mVerticalAlignment == ALIGN_CENTER) { fm.bottom += need / 2; fm.top -= need / 2; } else { fm.top -= need; } // } } @Override public void updateDrawState(final TextPaint tp) { } } class SpaceSpan extends ReplacementSpan { private final int width; private final int color; private SpaceSpan(final int width) { this(width, Color.TRANSPARENT); } private SpaceSpan(final int width, final int color) { super(); this.width = width; this.color = color; } @Override public int getSize(@NonNull final Paint paint, final CharSequence text, @IntRange(from = 0) final int start, @IntRange(from = 0) final int end, @Nullable final Paint.FontMetricsInt fm) { return width; } @Override public void draw(@NonNull final Canvas canvas, final CharSequence text, @IntRange(from = 0) final int start, @IntRange(from = 0) final int end, final float x, final int top, final int y, final int bottom, @NonNull final Paint paint) { Paint.Style style = paint.getStyle(); int color = paint.getColor(); paint.setStyle(Paint.Style.FILL); paint.setColor(this.color); canvas.drawRect(x, top, x + width, bottom, paint); paint.setStyle(style); paint.setColor(color); } } class CustomQuoteSpan implements LeadingMarginSpan { private final int color; private final int stripeWidth; private final int gapWidth; private CustomQuoteSpan(final int color, final int stripeWidth, final int gapWidth) { super(); this.color = color; this.stripeWidth = stripeWidth; this.gapWidth = gapWidth; } public int getLeadingMargin(final boolean first) { return stripeWidth + gapWidth; } public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir, final int top, final int baseline, final int bottom, final CharSequence text, final int start, final int end, final boolean first, final Layout layout) { Paint.Style style = p.getStyle(); int color = p.getColor(); p.setStyle(Paint.Style.FILL); p.setColor(this.color); c.drawRect(x, top, x + dir * stripeWidth, bottom, p); p.setStyle(style); p.setColor(color); } } class CustomBulletSpan implements LeadingMarginSpan { private final int color; private final int radius; private final int gapWidth; private Path sBulletPath = null; private CustomBulletSpan(final int color, final int radius, final int gapWidth) { this.color = color; this.radius = radius; this.gapWidth = gapWidth; } public int getLeadingMargin(final boolean first) { return 2 * radius + gapWidth; } public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir, final int top, final int baseline, final int bottom, final CharSequence text, final int start, final int end, final boolean first, final Layout l) { if (((Spanned) text).getSpanStart(this) == start) { Paint.Style style = p.getStyle(); int oldColor = 0; oldColor = p.getColor(); p.setColor(color); p.setStyle(Paint.Style.FILL); if (c.isHardwareAccelerated()) { if (sBulletPath == null) { sBulletPath = new Path(); // Bullet is slightly better to avoid aliasing artifacts on mdpi devices. sBulletPath.addCircle(0.0f, 0.0f, radius, Path.Direction.CW); } c.save(); c.translate(x + dir * radius, (top + bottom) / 2.0f); c.drawPath(sBulletPath, p); c.restore(); } else { c.drawCircle(x + dir * radius, (top + bottom) / 2.0f, radius, p); } p.setColor(oldColor); p.setStyle(style); } } } @SuppressLint("ParcelCreator") class CustomTypefaceSpan extends TypefaceSpan { private final Typeface newType; private CustomTypefaceSpan(final Typeface type) { super(""); newType = type; } @Override public void updateDrawState(final TextPaint textPaint) { apply(textPaint, newType); } @Override public void updateMeasureState(final TextPaint paint) { apply(paint, newType); } private void apply(final Paint paint, final Typeface tf) { int oldStyle; Typeface old = paint.getTypeface(); if (old == null) { oldStyle = 0; } else { oldStyle = old.getStyle(); } int fake = oldStyle & ~tf.getStyle(); if ((fake & Typeface.BOLD) != 0) { paint.setFakeBoldText(true); } if ((fake & Typeface.ITALIC) != 0) { paint.setTextSkewX(-0.25f); } paint.getShader(); paint.setTypeface(tf); } } class CustomImageSpan extends CustomDynamicDrawableSpan { private Drawable mDrawable; private Uri mContentUri; private int mResourceId; private CustomImageSpan(final Bitmap b, final int verticalAlignment) { super(verticalAlignment); mDrawable = new BitmapDrawable(mContext.getResources(), b); mDrawable.setBounds( 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight() ); } private CustomImageSpan(final Drawable d, final int verticalAlignment) { super(verticalAlignment); mDrawable = d; mDrawable.setBounds( 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight() ); } private CustomImageSpan(final Uri uri, final int verticalAlignment) { super(verticalAlignment); mContentUri = uri; } private CustomImageSpan(@DrawableRes final int resourceId, final int verticalAlignment) { super(verticalAlignment); mResourceId = resourceId; } @Override public Drawable getDrawable() { Drawable drawable = null; if (mDrawable != null) { drawable = mDrawable; } else if (mContentUri != null) { Bitmap bitmap; try { InputStream is = mContext.getContentResolver().openInputStream(mContentUri); bitmap = BitmapFactory.decodeStream(is); drawable = new BitmapDrawable(mContext.getResources(), bitmap); drawable.setBounds( 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() ); if (is != null) { is.close(); } } catch (Exception e) { Log.e("sms", "Failed to loaded content " + mContentUri, e); } } else { try { drawable = ContextCompat.getDrawable(mContext, mResourceId); drawable.setBounds( 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() ); } catch (Exception e) { Log.e("sms", "Unable to find resource: " + mResourceId); } } return drawable; } } abstract class CustomDynamicDrawableSpan extends ReplacementSpan { static final int ALIGN_BOTTOM = 0; static final int ALIGN_BASELINE = 1; static final int ALIGN_CENTER = 2; static final int ALIGN_TOP = 3; final int mVerticalAlignment; private CustomDynamicDrawableSpan() { mVerticalAlignment = ALIGN_BOTTOM; } private CustomDynamicDrawableSpan(final int verticalAlignment) { mVerticalAlignment = verticalAlignment; } public abstract Drawable getDrawable(); @Override public int getSize(@NonNull final Paint paint, final CharSequence text, final int start, final int end, final Paint.FontMetricsInt fm) { Drawable d = getCachedDrawable(); Rect rect = d.getBounds(); if (fm != null) { // LogUtils.d("fm.top: " + fm.top, // "fm.ascent: " + fm.ascent, // "fm.descent: " + fm.descent, // "fm.bottom: " + fm.bottom, // "lineHeight: " + (fm.bottom - fm.top)); int lineHeight = fm.bottom - fm.top; if (lineHeight < rect.height()) { if (mVerticalAlignment == ALIGN_TOP) { fm.top = fm.top; fm.bottom = rect.height() + fm.top; } else if (mVerticalAlignment == ALIGN_CENTER) { fm.top = -rect.height() / 2 - lineHeight / 4; fm.bottom = rect.height() / 2 - lineHeight / 4; } else { fm.top = -rect.height() + fm.bottom; fm.bottom = fm.bottom; } fm.ascent = fm.top; fm.descent = fm.bottom; } } return rect.right; } @Override public void draw(@NonNull final Canvas canvas, final CharSequence text, final int start, final int end, final float x, final int top, final int y, final int bottom, @NonNull final Paint paint) { Drawable d = getCachedDrawable(); Rect rect = d.getBounds(); canvas.save(); float transY; int lineHeight = bottom - top; // LogUtils.d("rectHeight: " + rect.height(), // "lineHeight: " + (bottom - top)); if (rect.height() < lineHeight) { if (mVerticalAlignment == ALIGN_TOP) { transY = top; } else if (mVerticalAlignment == ALIGN_CENTER) { transY = (bottom + top - rect.height()) / 2; } else if (mVerticalAlignment == ALIGN_BASELINE) { transY = y - rect.height(); } else { transY = bottom - rect.height(); } canvas.translate(x, transY); } else { canvas.translate(x, top); } d.draw(canvas); canvas.restore(); } private Drawable getCachedDrawable() { WeakReference wr = mDrawableRef; Drawable d = null; if (wr != null) { d = wr.get(); } if (d == null) { d = getDrawable(); mDrawableRef = new WeakReference<>(d); } return d; } private WeakReference mDrawableRef; } class ShaderSpan extends CharacterStyle implements UpdateAppearance { private Shader mShader; private ShaderSpan(final Shader shader) { this.mShader = shader; } @Override public void updateDrawState(final TextPaint tp) { tp.setShader(mShader); } } class ShadowSpan extends CharacterStyle implements UpdateAppearance { private float radius; private float dx, dy; private int shadowColor; private ShadowSpan(final float radius, final float dx, final float dy, final int shadowColor) { this.radius = radius; this.dx = dx; this.dy = dy; this.shadowColor = shadowColor; } @Override public void updateDrawState(final TextPaint tp) { tp.setShadowLayer(radius, dx, dy, shadowColor); } } } ================================================ FILE: app/src/main/res/drawable/bg_btn.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_java_demo.xml ================================================