自定义 ImageSpan 之行居中,可设置左右间距
实现功能:
- Image 所在行居中显示
- 可以设置图片和前后文本的间距
1 | import android.content.Context |
使用场景
1 | val spanStr = SpannableStringBuilder("#") |
实现功能:
1 | import android.content.Context |
使用场景
1 | val spanStr = SpannableStringBuilder("#") |
Android 10 (API 级别 29) 及更高版本中提供深色主题背景。深色主题背景具有诸多优势:
将应用的主题背景(通常可在 res/values/styles.xml 中找到)设置为继承 DayNight 主题背景。
1 | <style name="AppTheme" parent="Theme.AppCompat.DayNight"> |
用到的资源和颜色需要在 -night 目录中重新配置一份。
在暗黑模式下,系统会优先从 -night 后缀的目录下找到对应的资源配置。
如果您的应用采用浅色主题背景,则 Force Dark 会分析应用的每个视图,并在相应视图在屏幕上显示之前,自动应用深色主题背景。
Force Dark 应用需要满足以下三个条件
Theme.Material.Light)android:forceDarkAllowed="true"如果您的应用使用深色主题(例如 Theme.Material),或者继承自 DayNight 主题背景,则系统不会应用 Force Dark。
在特定 View 上停用 Force Dark,可以通过 android:forceDarkAllowed 布局属性或 setForceDarkAllowed() 。
如要切换主题背景,请调用 AppCompatDelegate.setDefaultNightMode()。
每个选项直接映射到以下某个 AppCompat.DayNight 模式:
注意:从 AppCompat v1.1.0 开始,setDefaultNightMode() 会自动重新创建任何已启动的 Activity。
当应用的主题背景发生更改(无论是通过系统设置还是 AppCompat)时,会触发 uiMode 配置变更。这意味着系统会自动重新创建 Activity。
在某些情况下,您可能希望应用处理配置变更。例如,您可能希望延迟配置变更时间,因为设备正在播放视频。
应用可以声明,每个 Activity 都可以处理 uiMode 配置变更,以自行处理深色主题背景的实现:
1 | <activity |
当某个 Activity 声明它会处理配置变更时,系统会在出现主题背景变更时调用该 Activity 的 onConfigurationChanged() 方法。
如要检查当前采用的是哪种主题背景,应用可以运行如下代码:
1 | val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK |
如果要判断当前手机系统是否是深色模式,可以使用以下代码:
1 | private fun isSystemNightMode(activity: Activity): Boolean { |
如何在 APP 内判断手机是否切换了系统深色模式?
前提是当前 APP 没有使用 android:configChanges="uiMode" 方式。
由于直接通过手机系统快捷设置切换深色模式时,会触发 activity 的 recreate()方法,导致 APP 的 activity 重建,但是在 activity 的 onPause()方法或者 APP 切换到后台时,获取到的深色模式状态已经是切换之后的了。所以,如果要判断 APP 使用过程中,系统是否切换深色模式,目前使用的方式是在进入 activity 时就记录一下当前的系统深色模式状态,然后,onStop 方法去检查系统的设置是否改变,并且把是否切换的值缓存在 APP 全局缓存(注意不能是 Activity 中)里。然后在 App 切换到前台的时候,再去获取缓存中的值,来判断上一次切换到后台时是否是因为系统切换了深色模式。
-night资源文件夹,配置对应的 color,drawable 和 layout 等官网深色主题背景: https://developer.android.google.cn/guide/topics/ui/look-and-feel/darktheme?hl=zh-cn
1 | import android.graphics.LinearGradient |
使用方式,调用扩展方法即可
textView.setHorizontalGradientTextColor(Color.RED, Color.GREEN)
原文地址:https://juejin.cn/post/7009204672225345549
Android 7.0之前,文件的Uri以 file:///形式提供给其他app访问。
Android 7.0之后,分享文件的Uri发生了变化。为了安全起见, file:///形式的Uri不能正常访问。官方提供了 FileProvider,FileProvider生成的Uri会以 content://的形式分享给其他app使用。
在7.0以前,为了访问 file:///形式的Uri,我们必须修改文件的权限。修改后的权限对所有app都是有效的,这样的行为是不安全的。 content://形式的Uri让Android的文件系统更安全,对于分享的文件,接收方app只拥有临时的权限,减少了我们app内部的文件被其他app恶意操作的行为。
在manifest文件 <application></application>标签中添加pvodier标签,配置如下。
1 | <manifest> |
android:name指定Provider的类名,使用官方提供的androidx.core.content.FileProvider。
android:authorities相当于一个用于认证的暗号,在分享文件生成Uri时,会通过它的值生成对应的Uri。。值是一个域名,一般格式为 <包名>.fileprovider</包名>。
android:exported设置为false,FileProvider不需要公开。
android:grantUriPermissions设置为true,这样就能授权接收端的app临时访问权限了。
在res/xml中创建一个资源文件(如果xml目录不存在,先创建),名字随便(一般叫file_paths.xml)。
1 | <paths xmlns:android="http://schemas.android.com/apk/res/android"> |
<paths></paths>必须有1个或多个子标签,每个子标签代表要请求的私有文件目录。不同的子标签代表不同的目录类型。
在 <provider></provider>标签中添加 <meta-data></meta-data>子标签。
设置 <meta-data></meta-data>的属性 android:name值为 android.support.FILE_PROVIDER_PATHS,属性 android:resouce的值为刚才我们创建的path文件名。
<paths></paths>的每个子标签必须有 path属性,代表content Uris的路径。 name不需要和path保持一样,只是一个名称。
子标签有以下几种。
1 | <files-path name="my_files" path="path" /> |
代表内部存储的files目录,与 Context.getFilesDir()获取的路径对应。
最终生成的Uri格式为:authorities/pathname/filename
示例:
1 | content: |
1 | <cache-path name="name" path="path" /> |
代表内部存储的cache目录,与 Context.getCacheDir()获取的路径对应。
1 | <external-path name="name" path="path" /> |
代表外部存储(sdcard)的cache目录,与 Environment.getExternalStorageDirectory()获取的路径对应。
1 | <external-files-path name="name" path="path" /> |
代表app的外部存储的根目录,与 Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)获取的路径对应。
1 | <external-cache-path name="name" path="path" /> |
代表app外部缓存区域的根目录,与 Context.getExternalCacheDir()获取的路径对应。
1 | <external-media-path name="name" path="path" /> |
代表app外部存储媒体区域的根目录,与 Context.getExternalMediaDirs()获取的路径对应。
注意: 这个目录只在API 21(也就是Android 5.0)以上的系统上才存在。
为了让其他app使用Content Uri,我们的app必须提前生成Uri。
1 | File file = new File(Context.getFilesDir(), "my_log"); |
这里注意获取目录,在配置paths时我们讲了,paths的子标签必须和获取目录的代码保持对应。这里我们用的是 Context.getFilesDir(),所以paths文件中必须包含 files-path子标签,不然别的app获取uri时会出现异常。
最终生成Uri是使用的 FileProvider.getUriForFile()。第一个参数就是 provider中设置的 authorities属性值。
1 | intent.putExtra(Intent.EXTRA_STREAM, contentUri); |
使用 Intent.setDate或 Intent.setClipData()。
1 | intent.setClipDataClipData.newRawUri("", contentUri) |
最后使用 startActivity(intent)启动分享操作。
分享一般只有这读取和写入2种权限,根据需要传入 Intent.addFlags()中。
1 | Intent intent = new Intent(Intent.ACTION_SEND); |
这是因为第三方库中存在很多重名的META-INF文件,在打包的时候去除即可
1 | android { |
在执行gradlew命令打包时遇到这个错误,肯定是https证书有问题。
解决方案:如果支持http的话就使用http
1 | Could not resolve com.bytedanceapi:ttsdk-ttmp:1.36.2.25.pcdn. |
把项目根目录的build.gradle文件中所有的https://artifact.bytedance.com/替换为http://artifact.bytedance.com/即可
升级到 Java 16 以上,AndroidStudio编译遇到错误。
Unable to make field private final java.lang.String java.io.File.path accessible: module java.base does not “opens java.io” to unnamed module @fb04536
解决方案一:
gradle-wrapper 属性中的 gradle 版本更改为 7.1.1(6.x 不支持 java 16)gradle.properties 中添加以下行1 | org.gradle.jvmargs=-Xmx1536m \ |
解决方案二:
升级build-gradlew版本。 将项目根目录的 build.gradle文件中classpath 'com.android.tools.build:gradle:4.2.2'
升级为classpath 'com.android.tools.build:gradle:7.2.1'
RecyclerView+SnapHelper实现ViewPager滑动效果
SnapHelper结合RecyclerView使用,能很方便的实现ViewPager滑动效果。SnapHelper是一个抽象类,Google内置了两个默认实现类,LinearSnapHelper和PagerSnapHelper。
使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动多个条目。
1 | LinearLayoutManager manager = new LinearLayoutManager(getContext()); |
使RecyclerView像ViewPager一样的效果,每次只能滑动一页。
1 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); |
Aria 中文文档: https://aria.laoyuyu.me/aria_doc/
版本:3.8.15
在Activity的onCreate、fragment的onCreate、java的构造函数中使用Aria.download(this).register()便可以实现注册。
Aria类仅一个私有的构造方法,无法实例化
1 | private Aria() {} |
Aria类中的2个主要静态方法 download, upload, 对应下载和上传两种类型
以下载方法为例,下载,在当前类中调用Aria方法,参数需要使用this,返回对象是 DownloadReceiver
1 | public static DownloadReceiver download(Object obj) { |
AriaManager#init()方法进行初始化1 |
|
1 | private Map<String, AbsReceiver> mReceivers = new ConcurrentHashMap<>(); |
DownloadReceiver#register()方法TaskSchedulers 事件调度器,用于处理任务状态的调度
1 | private Map<String, Map<TaskEnum, Object>> mObservers = new ConcurrentHashMap<>(); |
1 | long taskId = Aria.download(this) |
01 DownloadReceiver.load()
1 HttpBuilderTarget.create()
2 BuilderController.create()
3 CmdHelper.createNormalCmd()
4 NormalCmdFactory.createCmd() -> StartCmd
5 EventMsgUtil.getDefault().post(StartCmd) -> mEventQueue.take()
6 EventMsgUtil.sendEvent()
7 StartCmd.executeCmd() -> AbsNormalCmd.startTask()
8 DTaskQueue.createTask(DTaskWrapper wrapper)
9 TaskWrapperManager.getInstance().putTaskWrapper(wrapper)
10 AbsTaskQueue.startTask()
11 DLoadExecutePool.putTask()
12 AbsTask.start()
13 HttpDLoaderUtil.start() -> AbsNormalLoaderUtil.start()
14 NormalLoader.run() -> AbsNormalLoader.run()
15 AbsNormalLoader.startFlow() -> NormalLoader.handleTask()// 启动单线程任务
16 NormalLoader.startThreadTask()
17 NormalTTBuilder.buildThreadTask()
18 ThreadTaskManager.getInstance().startThread()
19 AbsNormalLoader.startTimer() // 启动进度获取定时器
21 ThreadTask.call() // 线程池执行任务回调
22 AbsThreadTaskAdapter.call()
23 HttpDThreadTaskAdapter.handlerThreadTask() // 正式建立Http连接,下载任务
24 HttpDThreadTaskAdapter.readNormal()
25 HttpDThreadTaskAdapter.handleComplete()
26 ThreadTask.updateCompleteState()
27 NormalThreadStateManager.callback -> STATE_COMPLETE
28 BaseListener.onComplete() // 对应的实体类是BaseDListener
29 BaseListener.sendInState2Target() // 将任务状态发送给下载器
M3U8ThreadTaskAdapter.readDynamicFile(InputStream is) // 动态长度文件读取方式
M3U8ThreadTaskAdapter.handleComplete() // 处理完成
ThreadTask.updateCompleteState() // 组装Message消息,发送给VodStateManager.callback -> Handler.Callback
VodStateManager.callback -> STATE_COMPLETE
BaseListener.onComplete() // 对应的实体类是M3U8Listener
1 | import android.content.Context |
1 | import android.content.Context |
设置recyclerView的layoutManager为自定义的TopScrollLinearLayoutManager,然后直接调用 smoothScrollToPosition() 方法就可以滚动到指定的位置并且置顶了。
1 | recyclerView.layoutManager = TopScrollLinearLayoutManager( |
一个简单的基于ExoPlayer的播放器。
ExoPlayer官网:https://exoplayer.dev/
使用ExoPlayer版本:2.18.2
实现功能:通过url播放视频,简易自定义controller,监听播放器状态变化,首帧时间打印,错误信息打印。
如果需要深层次的UI定制,建议不要用exo_ui库下面的布局,用SurfaceView和TextureView完全自定义。具体代码如下:
1 |
|
exo_player_fragment.xml
1 |
|
custom_player_control_view.xml
1 |
|
工具类,提供了系统栏高度和屏幕宽高获取方法
1 |
|
1 | val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager |
将 NetworkCallback 类与 ConnectivityManager.registerDefaultNetworkCallback(NetworkCallback) 及 ConnectivityManager.registerNetworkCallback(NetworkCallback) 结合使用。
1 | val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager |
1 | private fun handle(caps: NetworkCapabilities?) { |
使用ColorMatrix设置灰度
1 | private fun setGrayPaint(view: View) { |
给首页Activity的decorView设置灰度Paint
1 | override fun onCreate(savedInstanceState: Bundle?) { |
这些控件由于不是跟activity公用一个window,需要各自单独处理灰度Paint。调用 setGrayPaint(view) 即可。
AndResGuard是一个帮助你缩小APK大小的工具,他的原理类似Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a。
AndResGuard不涉及编译过程,只需输入一个apk(无论签名与否,debug版,release版均可,在处理过程中会直接将原签名删除),可得到一个实现资源混淆后的apk(若在配置文件中输入签名信息,可自动重签名并对齐,得到可直接发布的apk)以及对应资源ID的mapping文件。
根据Android的编译流程,所有资源ID已经被编译成32位int值。这说明我们并不需要去修改xml与java,因为在编译过程已经被R.java所替换,我们直接修改resources.arsc的二进制数据,不改变打包流程,只要在生成resources.arsc之后修改它,同时重命名资源文件。
build.gradle中,添加插件的依赖1 | buildscript { |
build.gradle中添加Res相关配置1 | plugins { |
使用Android Studio的同学可以在 andresguard 下找到相关的构建任务; 命令行可直接运行./gradlew resguard[BuildType | Flavor], 这里的任务命令规则和assemble一致。
在设置sevenzip时, 你只需设置artifact或path, 支持同时设置,总以path的值为优先。
如果没有配置finalApkBackupPath,最终结果会覆盖assemble[BuildType | Flavor]的输出APK。如果配置则输出至finalApkBackupPath配置路径。
如果项目中使用了font资源,需要配置mappingFile = file("./resource_mapping.txt"),同时在app目录的resource_mapping.txt文件中添加
1 | res path mapping: |
white_list.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md查看更多sdk的白名单配置使用说明:https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md
white_list.md:https://github.com/shwenzhang/AndResGuard/blob/master/doc/white_list.md
由于 Android 动态权限申请是一个交互比较复杂的模块,整个申请的流程也比较长,所以,写了一个工具来封装了一个,也具体的实现了一个流程。
由于每个App的Ui风格不一致,所以没有把Toast和弹框封装进工具,等后期有好的想法再优化。
主要封装类 PermissionManager
根据业务需要用到安卓定义的高危权限,需要去动态申请权权限。通常是在Activity 和 Fragment 组件中发起。
1 检查权限授权状态
1 | if (PermissionManager.hasPermissions(this, Manifest.permission.READ_PHONE_STATE)) { |
2 申请权限,根据合规化通常需要先弹框
1 | private fun showRequestPermissionDialog(message: String, vararg perm: String) { |
3 处理权限申请结果
在 onRequestPermissionsResult 回调接口中,调用 PermissionManager.onRequestPermissionsResult 方法,并且实现 PermissionManager.OnPermissionResultCallback 这个回调接口。
1 | override fun onRequestPermissionsResult( |
4 当用户‘拒绝且不再询问’,引导去手机设置
1 | private fun showSettingsDialog() { |
5 检查手机设置后的权限申请结果
1 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
PermissionXPermissionX is an extension Android library that makes Android runtime permission request extremely easy. You can use it for basic permission request occasions or handle more complex conditions, like showing rationale dialog or go to app settings for allowance manually.
.idea .git .gitlab 等文件夹,.gradle 可以不删settings.gradle,修改项目名称 rootProject.name = "xxx"这一步是要将包名从 com.sample1.android 重命名为 com.sample2.android
Compat Middle Packages 前面的勾sample1 包名,Shift + F6重命名Rename Package.aidl 文件的包名路径和import的路径build.gradle 的 applicationId "com.sample2.android",并 syncps: 不清楚为啥Kotlin的扩展函数还需要重新手动导入
到上面这一步,不出意外的话已经可以编译成功并且跑起来了。编译成功之后可以先推送本地项目到代码库。
远程代码库新建项目,获取新项目git地址,然后执行下面操作。
1 | cd existing_folder |
下面就是修改名称,URL,替换功能啥的。
现在应用市场对新应用的审核非常严格。为了过审,可能需要做很多相关的工作
Shift + F6重命名时没有Rename Package选项如果Shift + F6重命名时没有Rename Package选项, 并且出现了以下提示,Package 'example' contains directories in libraries which cannot be renamed. Do you want to rename current directory or all directories in project?,
这是因为依赖包中也存在example这个包名,导致无法直接重命名包名。解决的方案分两种:
方案1. 暂时先移除包含example包名的依赖包,等重命名之后重新添加到项目中
方案2. 只能新建package,然后将要rename的包拖动到新的package中,或者F6移动
PS:因为无法重命名,刚刚经历了手动移动十几个module的package包路径的痛苦历程。
示例代码如下:
1 | import android.content.Context; |
在 attrs.xml 文件中定义控件的圆角dp值属性:
1 | <declare-styleable name="RoundImageView"> |
使用示例
1 | <com.xx.ui.widget.RoundImageView |
原文地址:https://www.cnblogs.com/baiqiantao/p/6890674.html
Installing Gradle: https://docs.gradle.org/current/userguide/installation.html
GRADLE_HOME:仅仅是为了可以在任意目录中执行 gradle 命令,没有特殊的意义GRADLE_USER_HOME:控制在命令行中执行 gradlew 命令时,gradle 下载的目录Gradle user home:控制在 IDEA 点击按钮执行各项 Task 等功能时,gradle 下载的目录User from gradle:控制在 IDEA 点击按钮执行各项 Task 等功能时,使用的 gradle 的版本设置环境变量 GRADLE_HOME 的目的,仅仅是为了方便在 Path 中指定 gradle 的位置。
GRADLE_HOME:D:_dev\gradle_GRADLE_HOME\gradle-6.7
Path:%GRADLE_HOME%\bin
将 gradle 添加到 Path 的目的是为了,可以在任意目录中执行 gradle 命令。
实际上,完全没必要设置环境变量 GRADLE_HOME,Do we really need GRADLE_HOME?
1 | gradle -v # 查看版本 |
其实 gradlew 只是一个 gradle 的封装(wrapper),gradlew = gradle wrapper,因为在项目根目录有 gradlew 和 gradlew.bat 这两个可执行文件,所以 能且仅能 在项目根目录中执行 gradlew 命令。
1 | D:\_dev\_code\as\Test> gradlew -v # 查看版本 |
这两个文件头部的注释也说明了他们的作用:
之所以添加这个 gradlew 脚本,是为了:
统一项目所使用的 gradle 版本,避免不同开发人员使用不同的 gradle 版本导致的兼容性问题
可以把 gradle-wrapper.properties 里面的下载 gradle 的地址切换到公司的公共空间上,以加快下载速度
1 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip |
设置环境变量 GRADLE_USER_HOME 的目的,是为了自定义下载 gradle 时的本地存储路径。
在命令行中执行 gradlew 命令(注意不是 gradle 命令)时,会将对应版本的 gradle 下载到此目录中。
下载 gradle 时,下载地址及版本由项目中的 /gradle/wrapper/gradle-wrapper.properties 决定。
1 | #Mon Nov 16 00:55:48 CST 2020 |
在 IDEA 的 File | Settings | Build, Execution, Deployment | Build Tools | Gradle 中,有一个 Gradle user home 的配置,其作用和环境变量 GRADLE_USER_HOME 类似,只不过该配置只是给 IDEA 使用的。譬如点击 gradle 窗口的各种 Task 按钮执行各项 Task 功能时。
注意:仅 IDEA 中的各种图形化操作会使用此配置,在 IDEA 的 Terminal 中执行 gradlew 命令时,使用的依旧是环境变量 GRADLE_USER_HOME。
在 IDEA 的 File | Settings | Build, Execution, Deployment | Build Tools | Gradle 中,有一个 User from gradle 的配置,它也是仅提供给 IDEA 使用的(对 gradlew 无效)。
其作用是,指定当前工程中 IDEA 所使用的 gradle 版本:
当勾选 gradle-wrapper.properties 时,使用 gradle-wrapper.properties 中指定的 gradle 版本。
为了防止和在 Terminal 中执行 gradlew 命令时使用的 gradle 版本不同,建议勾选此配置(也是默认配置)。
当勾选 Specified location 时,使用指定目录下的 gradle 版本。
如果 gradle 下载很慢,就可以勾选此配置,以便使用指定本地下载好的 gradle 版本。
不管在 IDEA 中怎么配置,在 Terminal 中执行 gradlew 命令时,所使用的 gradle 版本都是由
gradle-wrapper.properties决定的,并且下载路径也都是由环境变量GRADLE_USER_HOME决定的。
这里的 JDK 指的是执行 Gradle 命令依赖的 JDK,并非 AndroidStudio 工程依赖的 JDK。
通过 File | Settings | Build, Execution, Deployment | Build Tools | Gradle 设置的 JDK,是在运行 IDEA 图形化按钮时使用的。
通过 gradle.properties 设置的 JDK,是在 Terminal 中执行 gradlew 命令时使用的。
1 | # MacOS的路径写法 |
注意:AGP 从 7.0.0-alpha02 版本起,需要使用 Java 11
1 | val intent = Intent() |
警告:爱加密加固之后的包,会把这个异常给吃掉,导致无法跳转,也无反应。
这种方式需要处理手机中不存在指定浏览器的情况
1 | try { |
由于华为鸿蒙系统已经没有Android默认的浏览器,所以此处必须要有异常处理,或者提前处理手机中不存在指定浏览器的情况。
市场上常用浏览器的包名和类名@20220630
1 | 华为: "com.huawei.browser/com.huawei.browser.BrowserMainActivity" |
根据滑动位置动态改变背景透明度,直接上代码:
1 | binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { |
https://developer.umeng.com/docs
qqzone_id_value 配置跟当前应用对应不上,PlatformConfig.setQQZone(qqzone_id_value, qqzone_secret_id_value)
项目地址:https://github.com/lzyzsd/JsBridge
使用参考:https://www.jianshu.com/p/7aea03838f19
解决方案:
App层销毁当前的WebView,重新加载一个新的WebView去loadUrl。
1 | // js-bridge register error |
需求背景:
因为各种原因,需要打开手机自带的应用商店
核心代码
1 |
|
测试代码:
1 | private fun initView() { |
当显示2个或2个以上文案时,每隔2秒切换气泡文案
核心实现代码如下:
1 |
|
由于动画要在顶部浮层,这样动画才能不被父类容器的大小所限制和切割,所以,直接在PopWindow中显示。
具体代码如下:
1 | import android.view.LayoutInflater |
添加布局代码如下:
1 |
|
drawable:
1 |
|
显示内容超出规定的行数之后,显示 展开 和 收起
1 | import android.content.Context; |
控件使用方式
1 | val tvContent = holder.getView<ExpandableTextView>(R.id.tv_content) |
1 | import android.content.ComponentName |
1 | class CenteredImageSpan(context: Context, drawableRes: Int) : ImageSpan(context, drawableRes) { |
1 | val spanStr = SpannableStringBuilder() |
Dev Doc
https://developer.android.google.cn/reference/androidx/core/content/FileProvider
在 androidx 包提供的 FileProvider 提供了 生成文件Uri 的功能。
在 manifest 文件中,声明一个 provider
1 | <manifest> |
在 res/xml/file_paths.xml 下配置可用的文件路径,FileProvider 只能生成配置了的文件Uri。每个你想要生成Uri的文件路径都需要在 paths 下面定义。
1 | <paths xmlns:android="http://schemas.android.com/apk/res/android"> |
和其他 app 共享一个文件,你需要生成一个Uri。
1 | File imagePath = new File(Context.getFilesDir(), "my_images"); |
getUriForFile() 返回一个 content URI content://com.mydomain.fileprovider/my_images/default_image.jpg.
1 | shareContentIntent.setClipData(ClipData.newRawUri("", contentUri)); |
Intent by calling setData().Intent.setFlags() with either Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or both.Intent to another app. Most often, you do this by calling setResult().1 | // 使用uri |
1 | java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/DCIM/Camera/**.jpeg |
问题分析
解决方案
官网下载: http://adbshell.com/downloads
命令参考: http://adbshell.com/commands
Android官网adb介绍: https://developer.android.google.cn/studio/command-line/adb?hl=zh_cn
Mumu adb常用指令指引: https://mumu.163.com/help/20210513/35047_947512.html
1 | ## 连接/断开连接网络电视 |
在某些情况下,您可能需要终止 adb 服务器进程,然后重启才能解决问题。例如,如果 adb 不响应命令,就可能会发生这种情况。
1 | ## 停止adb服务 |
命令格式:adb -s <serialNumber> command,如:adb -s 127.0.0.1:7555 shell pm list package
可以通过 adb devices 获取目标设备的serialNumber
1 | $ adb devices |
1 | ## 安装apk |
所有应用包名列表
adb shell pm list packages
第三方应用包名列表
adb shell pm list packages -3
系统应用包名列表
adb shell pm list packages -s
根据某个关键字查找包
adb shell pm list packages |grep tencent
查看包安装位置
adb shell pm list packages -f |grep tencent
1 | ## 正在运行应用包名和Activity |
adb shell am start -n 应用包名/应用Activity类名
若想查看启动应用耗时,则可使用adb shell am start -W 应用包名/应用Activity类名
adb shell am force-stop 应用包名
adb shell dumpsys package 应用包名 | findstr version
adb shell pm clear 应用包名
按键输入
adb shell input keyevent 键值
如:adb shell input keyevent 3表示按下HOME键,其他键值对应键位可网上搜索
字符输入
adb shell input text 字符
如:adb shell input text test则表示输入了test字符串
ps:字符不支持中文
鼠标点击
adb shell input tap X Y
X Y分别为当前屏幕下的x和y轴坐标值
鼠标滑动
adb shell input swipe X1 Y1 X2 Y2
X1 Y1 和X2 Y2分别为滑动起始点的坐标
adb push myfile.txt /sdcard/myfile.txt
adb pull /data/test.apk D:\
将模拟器当前显示截图
adb shell screencap /data/screen.png
将截图文件下载至电脑
adb pull /data/screen.png C:\
开始录制
adb shell screenrecord /data/test.mp4
结束录制
可按CTRL+C结束录制
导出视频文件
adb pull /data/test.mp4 C:\
1 | ## 设备型号 |
| 命令 | 功能 |
|---|---|
| adb get-state | 判断设备状态 |
| adb devices | 显示连接到计算机的设备 |
| adb get-serialno | 获取设备的序列号 |
| adb reboot | 重启设备 |
| adb reboot bootloader | 重启设备进入fastboot模式 |
| adb reboot recovery | 重启设备进入recovery模式 |
进入 JDK/bin,输入命令:
1 | keytool -genkey -alias 密钥别名 -keyalg RSA -keysize 1024 -validity 36500 -keystore D:\test.jks -storetype pkcs12 |
参数说明:
-genkeypair 生成一条密钥对(由私钥和公钥组成)
-keystore 密钥库名字及存储位置(默认当前目录)
-alias 密钥对的别名(密钥库可以存在多个密钥对,用于区分不同密钥对)
-validity 密钥对的有效期(单位:天)
-keyalg 生成密钥对的算法(常用 RSA/DSA ,DSA 只用于签名,默认采用DSA )
提示:可重复使用此命令,在同一密钥库中创建多条密钥对
进入 JDK/bin,输入命令:
1 | keytool -v -list -keystore D:\test.jks |

部分应用商店需要签名文件的md5,在 AndroidStudio 中执行gradlew命令
1 | gradlew signingReport |

CrashHandler 处理 Java 异常流程:
CrashHandler 源码如下
1 | import android.os.Build |
使用方式:
在 Application 的 onCreate() 方法中, 调用 RrCrashHandler.install(BuildConfig.DEBUG, AppUtils.isMainProgress(this))
参考链接:
https://github.com/android-notes/Cockroach
https://github.com/android-notes/Cockroach/blob/master/%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md
SmartRefreshLayout 嵌套 ViewPager2 上拉加载更多,在 finishLoadMore() 方法之后,底部加载 Loading 位置会多出一段空白不消失。
解决方案:
1 | smartRefreshLayout.setEnableScrollContentWhenLoaded(false) |
自定义下拉刷新头部,使用 PAGView 做动画,可以在 onMoving(boolean b, float v, int i, int i1, int i2) 方法中设置 pagView.setProgress(v); 添加手势动画。
1 | import android.content.Context; |
1、msg 可空String解析 jsonReader.peek() == JsonToken.NULL
1 | import com.google.gson.Gson |
埋点需求,Android App 需要在onResume 和 onPause 方法中计算浏览的时长,同时上报浏览的进度。
浏览进度Rate具体的计算方式的具体过程:
1、在滚动屏幕过程中,通过 textContent?.viewTreeObserver?.addOnScrollChangedListener 来记录屏幕滚动的位置
2、在滚动监听里通过textContent?.getLocationOnScreen(location)获取在屏幕的具体位置,同时,
计算出visibleHeight = screenHeight - location[1] 当前文本的可见高度
3、当可见高度超过目标的高度,则认为已经全部浏览,rate = 100% ,同时移除滚动监听textContent?.viewTreeObserver?.removeOnScrollChangedListener(mScrollChangeListener)
核心代码如下:
1 | private var totalHeight: Int = 0 |
提供了注册加载更多,和判断是否不足一屏等工具方法
1 | import androidx.recyclerview.widget.GridLayoutManager |
java.lang.AssertionError: Could not delete caches dirCreateProcess error=206, El nombre del archivo o la extensión es demasiado largo
Caused by: java.lang.AssertionError: Could not delete caches dir YourProjectPath\build\kotlin\compileDebugTestingKotlin
临时解决
打开任务管理器,结束 java.exe 或者 OpenJDK Platform Binary
降级 Android Studio
Notice: This happens with the newer AndroidStudio 4.2.x.
Google hasn’t provide us a fix, so you’ll need to downgrade to an older version which works for you. 4.1.3 seems to be working fine.
Please close other application using ADB:Monitor, DDMS, EclipWarning:debug info can be unavailable. Please close other application using ADB:Monitor, DDMS, Eclipse.
方案一:
1 | adb usb |
方案二:
打开任务管理器,结束adb.exe进程。
方案三:
重启 adb 服务
1 | adb kill-server |
能安装apk却无法在Logcat查看log,即使重启Android studio,重启adb服务都无法解决。最后通过重启手机搞定
platform-tools/api/api-versions.xml java.io.IOException: Stream closed在 android studio 更新到 v2020.3.1 后遇到
1 | cannot load api descriptions from ../Android/android-sdk/platform-tools/api/api-versions.xml java.io.IOException: Stream closed |
问题的原因与类SdkUtils (请参阅the source file)相关。SdkUtils类具有对文件platform-tools/api/api-versions.xml的硬引用,但是在最新的平台工具(31.0.3)中,该文件不再存在。
从platforms/android-31/data/api-versions.xml复制文件到platform-tools/api/api-versions.xml。
如果是CI编译,可以尝试以下脚本:
1 | steps: |
Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.升级android sdk api 版本到31,适配android 12 ,遇到这个问题。当前开发环境:
android studio 版本: 2020.3.1
AGP 版本: 4.1.2 (classpath "com.android.tools.build:gradle:4.1.2")
SDK 版本
1 | android { |
SDK Manager更新对应版本都正常下载,编译过程出现异常
1 | Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager. |
是 Build Tools 升级之后,DX 变成了 D8。而 AGP 4.x 的版本使用的还是DX。
解决的方案:
1 | change below to your Android SDK path |
将 C:\Users\user\AppData\Local\Android\Sdk\build-tools\31.0.0\d8.bat 改为 dx.bat
将 C:\Users\user\AppData\Local\Android\Sdk\build-tools\31.0.0\lib\d8.jar 改为 dx.jar
PS:也可以尝试升级 AGP 到 7.x
1 | java.lang.IllegalStateException: FragmentManager is already executing transactions |
解决方案
如果在 Fragment 中使用 ViewPager2,那么 FragmentStateAdapter 应该使用 childFragmentManager。将
1 | FragmentStateAdapter viewPagerAdapter = new FragmentStateAdapter(getActivity().getSupportFragmentManager(), titles); |
改为
1 | FragmentStateAdapter viewPagerAdapter = new FragmentStateAdapter(getChildFragmentManager(), titles); |
原因分析:
因为 FragmentStateAdapter 会保存所有 Fragment 实例,当调用 Adapter.notifyDataSetChanged() 方法时,Fragment 并没有走 onCreate 方法。
解决方案:
方案一(这个方法会导致内存泄漏,不推荐)
在调用 notifyDataSetChanged 之前,清空 FragmentStateAdapter 的 Fragment 列表。
方案二
重写 getItemId() containsItem() 这两个方法,并确保 getItemId() 的值是唯一的。
1 | override fun createViewPagerAdapter(): RecyclerView.Adapter<*> { |
1 | java.lang.IllegalStateException: Design assumption violated. |
解决方案:
如果重写了 getItemId() containsItem() 这两个方法,确保 getItemId() 的值是唯一的。代码同 0x02
原因分析:
同方向滚动事件被 ViewPager2 拦截了。
解决方案:
采用官方提供的自定义 NestedScrollableHost 来包一层 RecyclerView
1 | import android.content.Context |
对应的 layout 代码:
1 |
|
自定义 NestedRecyclerView 的分发事件通过 requestDisallowInterceptTouchEvent() 方法来限制父布类的拦截事件
1 | public class NestedRecyclerView extends RecyclerView { |
降级,使用 ViewPager 来嵌套 RecyclerView ,可以避免事件冲突,亲测有效。
1 | import android.os.Bundle |
在 Fragment 中使用 ViewPager 的时候,切换 Fragment 导致 ViewPager 无法正确恢复异常
1 | java.lang.IllegalStateException: Fragment no longer exists for key f1: unique id 55efaee5-a65c-4e57-9281-7c8f8f6e4156 |

在这个页面中,内容列表使用 ViewPager 嵌套 Fragment 实现,并和时间选择 Tab 绑定。切换【即将上线】和【播出时间表】Tab,实际是使用 FragmentManager 的 replace 方法,动态切换两个 Fragment,然后就报了上面的异常。
网上流行的解决方案是使用 FragmentPagerAdapter 或者添加
1 |
|
但是这样处理会导致 ViewPager 中的 fragments 全部无法恢复,导致 ViewPager 白屏。
本例中的解决方案是:
在切换【即将上线】和【播出时间表】Tab 时,不使用 FragmentManager 的 replace 方法,而采用动态的 Hide 和 show 方法暂时规避 Fragment 被回收的问题。
1 | java.lang.IllegalArgumentException: Called attach on a child which is not detached: BaseViewHolder{2b241e1 position=12 id=-1, oldPos=-1, pLpos:-1} androidx.recyclerview.widget.RecyclerView{afecb06 VFED..... ......ID 0,0-1080,2055 #7f09236e app:id/recycler_view_xxx}, adapter:com.xxxx.adapter.XxxAdapter@cfc75c7, layout:androidx.recyclerview.widget.LinearLayoutManager@24af7f4, context:com.xxxx.XxxActivity@1ed75e2 |
问题分析
对同一个 position 位置同时进行notifyItemRemoved(position) 和 notifyItemInserted(position) 操作导致。
解决方案
避免同时对同一个位置先 notifyItemRemoved 再 notifyItemInserted,使用 notifyItemChanged。
1 | adapter?.notifyItemChanged(position) |
当RecyclerView属性设置为wrap_content+maxHeight时,maxHeight没有效果。
问题分析
当RecyclerView的LayoutManager#isAutoMeasureEnabled()返回true时,RecyclerView高度取决于children view的布局高度,并非取决于RecyclerView自身的测量高度。
解决方案
因此,我们只需要重写LayoutManager的public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)方法即可为RecyclerView设置最大宽高。
1 | recyclerView.layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, false) { |
作者:猫爸iYao
链接:https://www.jianshu.com/p/0dec79ff70df
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1 | Thread Name: 'main' |
问题分析
情况1 Parcelable 对象为空,反序列化异常
情况2 Parcelable 序列化和反序列化的字段和顺序没有完全对应
情况3 自定义View的数据保存与恢复
解决方案
情况1 Parcelable 对象在序列化和反序列化增加 null 值判断
情况2 Parcelable 对象 read 和 write 字段的类型和顺序保持一直
1 | public class Account implements Parcelable { |
情况3 自定义View的数据保存与恢复
1 | override fun onSaveInstanceState(): Parcelable? { |
1 | java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 542688 bytes |
问题分析
1 Intent 传递的数据过大。
2 onSaveInstance 保存的数据过大。
解决方案
尽可能的使用少量的数据。
大数据考虑持久化和其他传递形式。
RecyclerView Item 嵌套 ScrollView
RecyclerView Item 嵌套 ScrollView 产生 Touch 事件冲突,通过自定义ScrollView来拦截和处理事件
自定义 ItemScrollView 代码如下
1 | import android.content.Context |
Windows 10 系统自带 PowerShell,美化教程
在Windows 应用商店安装,或者Github
1、安装posh-git、oh-my-posh和Get-ChildItemColor(美化ls命令):
在powershell管理员模式下:
1 | Install-Module posh-git -Scope CurrentUser |
2、设置修改powershell的配置文件:
1 | if (!(Test-Path -Path $PROFILE )) { New-Item -Type File -Path $PROFILE -Force } |
输入内容:
1 | Import-Module DirColors |
其中主题名可以在下面的路径里找到,可以自行切换主题。
1 | C:\Program Files\WindowsPowerShell\Modules\oh-my-posh\3.163.0\themes |
在Fluent Terminal设置 powerline 字体和字体大小。
在文件夹中打开:
1 | I 在文件夹中打开 |
在当前文件夹打开PowerShell:
空白处 Shift + 鼠标右键,在弹出的菜单中选择PowerShell.
1 | code . |
问题分析
监听返回键和音量键,重载OnKeyDown()方法,部分机型会失效。
解决方案
给相应的Dialog监听setOnKeyListener()。
1 | // 解决不同机型版本兼容问题,onKeyDown 可能被拦截 |
注意区分keycode,防止业务层重复处理
方案一:延迟弹出软键盘
在dialog显示之后,延迟200ms再显示软键盘
1 | //强制显示或者关闭系统键盘 |
方案二:设置 SoftInputMode 为 SOFT_INPUT_STATE_ALWAYS_VISIBLE
1 | getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); |
问题分析
一般情况下,在onPause或者dismiss方法直接调用hideKeyboard就可以
1 | override fun onPause() { |
但是,在某些时候还是会存在关闭不成功的情况。这是由于Dialog下面的Activity或Fragment存在EditText等抢占焦点,导致在DialogFragment在调用dismiss方法时,键盘已经被抢占焦点,所以无法关闭。
解决方案
在DialogFragment的dismiss方法回调
1 | override fun onDismiss(dialog: DialogInterface) { |
在前一个Activity或者Fragment中重新关闭键盘。
1 | // 消除弹框遗留下来的keyboard |
1 | import android.content.res.Resources |
这个自定义布局要求显示为 系列名称... + 第一季 ,后面的季内容显示完全,紧贴系列名称显示,系列名称在布局不允许的时候可以部分显示。
1 | /** |
Android专栏-BaseQuickAdapterHelper
1 | import android.view.View |
1 使用自定义 AlertDialog 实现
2 window?.setFlags 设置 dialog 的样式
3 window?.attributes 设置 dialog 的位置
4 返回键监听,兼容机型需要使用 setOnKeyListener
5 按一次音量键回调多次的问题,KeyEvent.action 事件分 KeyEvent.ACTION_UP 和 KeyEvent.ACTION_DOWN
6 音量加减需要获取系统音量 max 值来手动控制,不同手机 max 值域不同
示例代码如下:
1 | import android.app.Activity |
自定义Dialog的Style
1 | <style name="VolumeDialog" parent="android:style/Theme.Dialog"> |
观察者模式 + Kotlin 泛型实现的简易版消息中心
1 | class MessageCenter<T> { |
1 | import android.os.Bundle |
1 | package com.dench.webviewlib |
情景一:如果访问的页面中要与Javascript交互,则webView必须设置支持Javascript
1 | val webSettings = webView.settings |
情景二:在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置
1 | // 在安卓5.0之后,默认不允许加载http与https混合内容,需要手动设置 |
其他情景: 设置domStorageEnabled 和背景色需要验证,暂时没遇到。
由于设置了 android:layerType="software"导致的 webview 卡顿。
关于三个layerType属性介绍:https://blog.csdn.net/a345017062/article/details/7478667
解决:
开启 Activity 硬件加速 ``android:hardwareAccelerated=”true”, 并且设置 webview 的android:layerType=”none”`。
Update your browser to view this website correctly.&npsb;Update my browser now