Android进阶之开发问题笔记

1 常用用法

1.1 adb用法

(1)配置adb环境变量:
①配置adb环境变量:正规方法
②配置adb环境变量:更简单方法-将SDK中对应的路径目录下的adb.exe,AdbWinApi.dll, ③AdbWinUsbApi.dll复制到C:\Users\Administrator下
(2)配置adb在高级设置中,在cmd窗口中和AS工具中Terminal直接使用adb命令
(3)学习链接: 一份超全超详细的 ADB 用法大全
(4)Android 进阶第一篇——善用工具

1.2 各版本对应的SDK版本

Android进阶之开发问题笔记_第1张图片
Android官网-信息中心

Android version history维基百科

1.3 Android Studio工具使用

1.3.1 Android Studio常用快捷键

Android Studio常用快捷键

1.3.2 Android Studio查看指定版本的源码

(1)下载某个版本对应的源码

(2)指定版本compileSdkVersion

android {
    compileSdkVersion 26
}

(3)双击shift
在这里插入图片描述

1.4 android.os.Build常用常量

Android之Build类.(Android获取手机配置信息 )

android.os.Build 常用常量

1.5 App常用图标尺寸规范汇总

Android必知必会-App 常用图标尺寸规范汇总

1.10 手机开发者常用工具

(1)包括:USB调试;不锁定屏幕、不使用锁屏显示布局边界调试GPU过度绘制GPU呈现模式分析不保留活动;启用严格模式,应用在主线程执行长时间操作闪烁屏幕;

(2)学习链接:Android调试系列之开发者选项常用功能

1.11 Android颜色值透明度百分比和十六进制对应关系

public void rgba() throws Exception {
        System.out.println("透明度 | 十六进制");
        System.out.println("---- | ----");
        for (double i = 1; i >= 0; i -= 0.01) {
            i = Math.round(i * 100) / 100.0d;
            int alpha = (int) Math.round(i * 255);
            String hex = Integer.toHexString(alpha).toUpperCase();
            if (hex.length() == 1) hex = "0" + hex;
            int percent = (int) (i * 100);
            System.out.println(String.format("%d%% | %s", percent, hex));
        }
    }
// 透明度 | 十六进制
100% | FF    89% | E3  79% | C9   69% | B0  59% | 96
99% | FC     88% | E0  78% | C7   68% | AD  58% | 94
98% | FA     87% | DE  77% | C4   67% | AB  57% | 91
97% | F7     86% | DB  76% | C2   66% | A8  56% | 8F
96% | F5     85% | D9  75% | BF   65% | A6  55% | 8C
95% | F2     84% | D6  74% | BD   64% | A3  54% | 8A
94% | F0     83% | D4  73% | BA   63% | A1  53% | 87
93% | ED     82% | D1  72% | B8   62% | 9E  52% | 85
92% | EB     81% | CF  71% | B5   61% | 9C  51% | 82
91% | E8     80% | CC  70% | B3   60% | 99  50% | 80
90% | E6

49% | 7D     39% | 63  28% | 4A   19% | 30  9% | 17
48% | 7A     38% | 61  28% | 47   18% | 2E  8% | 14
47% | 78     37% | 5E  27% | 45   17% | 2B  7% | 12
46% | 75     36% | 5C  26% | 42   16% | 29  6% | 0F
45% | 73     35% | 59  25% | 40   15% | 26  5% | 0D
44% | 70     34% | 57  24% | 3D   14% | 24  4% | 0A
43% | 6E     33% | 54  23% | 3B   13% | 21  3% | 08
42% | 6B     32% | 52  22% | 38   12% | 1F  2% | 05
41% | 69     31% | 4F  21% | 36   11% | 1C  1% | 03
40% | 66     30% | 4D  20% | 33   10% | 1A  0% | 00

透明度百分比和十六进制对应关系计算方法

1.12 安卓开发插件推荐

安卓开发插件推荐

1.13 Android强制弹出,隐藏输入法

Android强制弹出,隐藏输入法

1.14 Android Activity切换动画常用实现方式

Android Activity切换动画常用实现方式

Android四大组件——Activity跳转动画、淡出淡入、滑出滑入、自定义退出进入

1.15 解决包重复

完美解决系列—解决包重复

1.17 JAVA环境变量的配置(win8)

JDK安装与环境变量配置

1.18 Android 防止多次重复点击的三种方法

Android 防止多次重复点击的三种方法

1.19 Android:一张图片占用多少内存

Android:一张图片占用多少内存

1.20 for循环break和continue的区别,附return

(1)break:break用于完全结束一个循环,跳出循环体执行循环后面的语句。
(2)continue:continue是跳过当次循环中剩下的语句,执行下一次循环。
(3)return:结束本方法的执行,返回。
(4)参考链接:
循环结构中break、continue、return和exit的区别
for循环的简介及break和continue的区别

1.23 如何选择compileSdkVersion, minSdkVersion和targetSdkVersion

(1)compileSdkVersion:告诉Gradle用哪个Android SDK版本编译你的应用,需要强调的是修改 compileSdkVersion 不会改变运行时的行为。当你修改了compileSdkVersion 的时候,可能会出现新的编译警告、编译错误,但新的compileSdkVersion不会被包含到APK 中:它纯粹只是在编译的时候使用。强烈推荐总是使用最新的 SDK 进行编译。
(2)minSdkVersion:则是应用可以运行的最低要求。
(3)targetSdkVersion:是Android提供向前兼容的主要依据,在应用的targetSdkVersion没有更新之前系统不会应用最新的行为变化。例如targetSdkVersion对应5.0版本,不会出现6.0的特性。
(4)例子(车载电视-不适用动态权限,把targetSdkVersion调至22)

ext {
    compileSdkVersion = 25
    buildToolsVersion = "25.0.0"
    minSdkVersion = 14
    targetSdkVersion = 22
}

(5)学习链接
manifest-----uses-sdk:各Android平台版本支持的API级别,包括各版本亮点

1.24 Fiddler对Android应用进行抓包

(1)抓包方法
csdn博客-如何使用Fiddler对Android应用进行抓包

Android平台HTTPS抓包全方案

(2)Fiddler显示IP
Fiddler显示IP的设置方法

(3)手机上怎么安装Fiddler证书
①打开浏览器,输入你电脑的ip+端口号,例如:http://192.168.1.103:8080/
Android进阶之开发问题笔记_第2张图片
②设置–>安全
Android进阶之开发问题笔记_第3张图片
③设置–>安全–>信任的凭证–>用户,是否有Fiddler证书。
Android进阶之开发问题笔记_第4张图片
④安装步骤,清除所有凭证—>从存储设备安装–>信任的凭证查询。

(4)过滤域名配置
Android进阶之开发问题笔记_第5张图片
(5)只抓某个域名
Android进阶之开发问题笔记_第6张图片
(6)问题解决
Fiddler实现手机抓包- 解决”creation of the root certificate was not successful

[Fiddler]No root certificate was found问题解决方案

1.23 安卓7.0及以上的Https请求抓包问题android:networkSecurityConfig

关于安卓7.0及以上的Https请求抓包问题android:networkSecurityConfig

关于android:networkSecurityConfig的添加

1.24 工具资源

(1)脑图XMind 8免费激活方法
脑图XMind 8免费激活方法

(2)Android apk反编译查看源码、反编译资源文件
Android apk反编译查看源码、反编译资源文件

1.25 Android 监听apk安装替换卸载广播

Android 监听apk安装替换卸载广播

1.26 Java 正则表达式

Java 正则表达式

求一个java中正则表达式,匹配所有标点符号,但除去‘-’和‘_’的

1.28 Android 官方UI控件Switch的使用方法

(1)效果
在这里插入图片描述
(2)学习链接
Android UI控件Switch的使用方法

Android View 仿iOS SwitchButton [Material Design]

1.29 TinyPng——接口扩展程序

TinyPng官网
TinyPng——接口扩展程序(我所用的最好的图片压缩)
Android进阶之开发问题笔记_第7张图片

1.30 实现动态模糊效果

教你一分钟实现动态模糊效果

1.31 …/配置

./是当前目录,../是父级目录,/是根目录

1.32 位运算(&、|、^、~、>>、<<)

位运算(&、|、^、~、>>、<<)

1.33 Android应用跳转手机QQ方式(聊天和加群)

Android应用跳转手机QQ方式(聊天和加群)

1.34 Android tools属性引用

Android tools属性引用

1.35 PackageManager详解

Android随笔之——PackageManager详解

1.36 Android客户端和服务端如何使用Token和Session

Android客户端和服务端如何使用Token和Session

1.37 批量导入aar包

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}

1.38 发布jar或aar到远程仓库

(1)Gradle之将Android项目开源到JCenter
Gradle之将Android项目开源到JCenter

(2)打包成aar并上传到Nexus搭建的maven仓库
Android 中打包成aar并上传到Nexus搭建的maven仓库

1.39 将library以及它依赖的module一起打包成一个完整aar的解决方案

将library以及它依赖的module一起打包成一个完整aar

// transitive的值为true,则远程依赖项的依赖项也将嵌入到最终的aar中。默认值为false
configurations.embed.transitive = true

dependencies {
    embed fileTree(dir: 'libs', include: ['*.jar', '*.aar'])

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.0.0"

    // 只编译,不嵌入最终aar包
    // 不建议使用implementation,因为该依赖可能与application的依赖版本不一致,使用implementation可能会导致R类找不到的问题
    compileOnly (name:'GDTSDK.unionNormal.4.150.1020',ext:'aar')
    compileOnly (name:'Baidu_MobAds_SDK-release',ext:'aar')

    // 只有用embed(name:'aa1', ext:'aar')才能打包进去
    // embed files("libs\aa1.aar")和embed fileTree(dir: 'libs', include: '*'),这些通用的语法都不报错,但打包不进去
    embed (name:'android-gif-drawable-1.2.6',ext:'aar')
    embed (name:'open_ad_sdk',ext:'aar')
    embed (name:'torch-adcore-5.14.3146',ext:'aar')

    api 'io.reactivex.rxjava2:rxandroid:2.1.0'
    api 'io.reactivex.rxjava2:rxjava:2.2.6'
    api 'com.github.bumptech.glide:glide:3.7.0'
}

1.41 Android网页WebView图片文件上传的问题

Android网页WebView图片文件上传的问题

1.42 查看代码混淆后的日志

(1)查看App下的Mapping.txt
…/app/build/outputs/mapping/normal下找到Mapping.txt

Android小技巧—查看代码混淆后的日志
(2)查看sdk下的proguardMapping.txt

1.43 解决android studio 3.6 中文乱码的问题

(1)问题Android进阶之开发问题笔记_第8张图片
(2)解决
①点击Help—>Edit Custom VM Options
Android进阶之开发问题笔记_第9张图片
②然后在打开的文件中添加一句
-Dfile.encoding=UTF-8
Android进阶之开发问题笔记_第10张图片
③然后重启Android Studio

(3)参考链接
Android Studio升级3.6 Build窗口出现中文乱码问题解决

解决android studio 3.6 中文乱码的问题

1.43 Android View的可见性检查方法

(1)案例

private NestedScrollView mScrollView;
 private Rect mScrollBounds = new Rect();
private AdNativeExpressActivityHolder mAdActivityHolder2 = null;

// 滑动监听
private NestedScrollView.OnScrollChangeListener mOnScrollChangeListener = (nestedScrollView, scrollX, scrollY, oldScrollX, oldScrollY) -> {
	// 可以先判断ScrollView中的mViewAdDivider2是不是在屏幕中可见
	mScrollView.getHitRect(mScrollBounds);
    if (mAdActivityHolder2 == null && mViewAdDivider2.getLocalVisibleRect(mScrollBounds)) {
    	initAd();
    }
};

(2)学习链接
Android View的可见性检查方法

2 问题记录

2.1 彻底解决INSTALL_FAILED_UPDATE_INCOMPATIBLE的安装错误

彻底解决INSTALL_FAILED_UPDATE_INCOMPATIBLE的安装错误

2.2 android.view.WindowManager$BadTokenException崩溃的4种情形

android.view.WindowManager$BadTokenException崩溃的4种情形

2.3 android.view.WindowLeaked:has leaked window com.android.internal.policy.PhoneWindow$DecorView–窗体句柄泄露问题

android.view.WindowLeaked: MainActivity has leaked window 
com.android.internal.policy.PhoneWindow$DecorView that was originally added here
at android.view.ViewRootImpl.(ViewRootImpl.java:464)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:306)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)                                                                   
at android.app.Dialog.show(Dialog.java:325)                                                                   

(1)原因:Dialog没有销毁前,它依附的Activity却销毁了,导致窗体泄露。
(2)解决方法:在Dialog调用show()时,判断当前依附的Activity是否已经销毁,在没有销毁时,才调用show()方法。

2.5 Error:Execution failed for task ‘:xiaoshuting:processReleaseFlavorDebugManifest’.> Manifest merger failed with multiple errors, see logs

2.5.1 分析思路

(1)AndroidStudio新版本这个按钮叫ToggleView,可以根据找到详细报错信息
Android进阶之开发问题笔记_第11张图片
(2)看一下Error:Execution failed for task ‘:app:processDebugManifest’.> Manifest merger failed with multiple errors, see logs这句话的上面显示的error日志,按照日志分析去找答案。

(3)参考链接
Manifest merger failed with multiple errors, see ?

2.5.2 案例分析1:清单文件有错,这种错不会在编译时指出来,但是在as中可以看到

(1)根据找到详细报错信息分析是清单文件有错
Android进阶之开发问题笔记_第12张图片
(2)问题分析:在715行和659行重复定义了FileProvider
Android进阶之开发问题笔记_第13张图片Android进阶之开发问题笔记_第14张图片
(3)问题解决:将resource的内容都配置到一个文件中:file_paths.xml,删除一个fileProvider



    
    
    
    

2.5.3 案例分析3:清单文件里面配置重复了

(1)截图
Android进阶之开发问题笔记_第15张图片
(2)原因/解决方法:在清单文件里面配置重复了,删掉一个,再同步工程。

2.5.4 案例分析3:sdk版本冲突

(1)问题的截图,如何查看具体的错误报告
Android进阶之开发问题笔记_第16张图片
(2)查看具体的错误报告,找到问题关键所在
Android进阶之开发问题笔记_第17张图片

Error:Execution failed for task ':app:processInternalDebugManifest'.
> Manifest merger failed : uses-sdk:minSdkVersion 11 cannot be smaller than version 14 declared in library [fm.jiecao:jiecaovideoplayer:4.5_preview1] E:\workspace\XWorld\app\build\intermediates\exploded-aar\fm.jiecao\jiecaovideoplayer\4.5_preview1\AndroidManifest.xml
	Suggestion: use tools:overrideLibrary="fm.jiecao.jcvideoplayer_lib" to force usage

(3)原因:引入的第三方库最低支持版本高于我的项目的最低支持版本,异常中的信息显示:我的项目的最低支持版本为14,而第三方库的最低支持版本为15,所以抛出了这个异常。

(4)解决方法
①将二者改成同一个就没有问题

②在AndroidManifest.xml文件中标签中添加:uses-sdk tools:overrideLibrary=“xxx.xxx.xxx”,为了项目中的AndroidManifest.xml和第三方库的AndroidManifest.xml合并时可以忽略最低版本限制。

// 其中的xxx.xxx.xxx为第三方库包名

// 如果存在多个库有此异常,则用逗号分割它们,例如:

// 举例:



    
    

(5)学习链接
Manifest merger failed with multiple errors, see logs问题处理思路

如何解决Suggestion: use tools:overrideLibrary="" to force usage

(6)小总结:遇到问题,关键是思路,如何排查问题的关键所在,针对关键问题找到解决方法。

2.5 java.lang.ClassNotFoundException: Didn’t find class “com.baidu.mobads.openad.FileProvider”

(1)截图
Android进阶之开发问题笔记_第18张图片
(2)解决
Android进阶之开发问题笔记_第19张图片

2.6 修复重复类错误:Program type already present: com.example.a.a.a.a.a

2.6.1 原因

引用两个jar包中重复类冲突了;

2.6.2 解决方法

(1)如果jar包是混淆过后,报错Program type already present: com.example.a.a.a.a.a,是因为有多个类被混淆成com.example.a.a.a.a.a,需要将jar包删除一个,或者修改jar包中的类名,或者查看jar包是否正确免混淆了这个类;

(2)二进制文件依赖项包含一个库,该库也作为直接依赖项包含在您的应用中。例如,您的应用声明直接依赖于库A和库B,但库A已在其二进制文件中包含库B。要解决此问题,请取消将库B作为直接依赖项。

2.6.3 在proguardMapping.txt中查看a.a.a.a.a对应的类

com.bytedance.a.a -> a.a.a.a:
    void b(java.lang.String,java.lang.String,java.lang.Throwable) -> a
    void a(java.lang.String,java.lang.String,java.lang.Throwable) -> b

2.6.4 学习链接

修复重复类错误

这两个jar包冲突,报错:Program type already present: com.tencent.a.a.a.a.a,应如何解决?

2.7 Android部分机型拍照-裁剪返回,resultCode=0

(1)问题描述:take photo -> 拍照 -> 确定 -> 截图 -> 保存,此时返回给onActivityResult的resultCode是0,截图无效。我查看图片储存的情况,拍完照是有存储好的,但截图后没有存储,图片变成了0byte。
(2)原因:裁减时打开图片的原路径uri与输出图片保存路径uri相同,产生冲突,导致裁减完成后图片的大小变成0Byte。

private void cropImageUri(Uri imageUri){
	Intent intent = new Intent("com.android.camera.action.CROP");
	// 裁减时打开图片的原路径uri
	intent.setDataAndType(imageUri, "image/*");
	// crop=true 有这句才能出来最后的裁剪页面.
	intent.putExtra("crop", "true");
	// 这两项为裁剪框的比例
	intent.putExtra("aspectX", aspectX);
	intent.putExtra("aspectY", aspectY);
	// 图片输出大小
	intent.putExtra("outputX", outputX);
	intent.putExtra("outputY", outputY);
	// 裁剪时是否保留图片的比例,这里的比例是1:1
	intent.putExtra("scale", true);
	 // 输出图片保存路径的uri
	intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
	intent.putExtra("return-data", false);
	// 返回图片格式
	intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
	// 如果小于要求输出大小,就放大
	intent.putExtra("noFaceDetection", true);
	// 如果小于要求输出大小,就放大
	intent.putExtra("scaleUpIfNeeded", true);
	startActivityForResult(intent, requestCode);
}

(3)解决问题:i图片的原路径和输出图片保存路径必须是不同的(可以修改图片的命名)

intent.setDataAndType(imageUri, “image/*”); // imageUri图片的原路径 
intent.putExtra(“output”, saveUri); //saveUri输出图片保存路径

(4)学习链接
拍照-裁剪,resultCode返回0
解决android有些机型截图返回 resultCode = 0的问题

2.8 there is no default constructor available in …

(1)截图情况
Android进阶之开发问题笔记_第20张图片Android进阶之开发问题笔记_第21张图片
(2)原因分析
A:一个类如果显式的定义了带参构造函数,那么默认无参构造函数自动失效;
B:一个类只要有父类,那么在它实例化的时候,一定是从顶级的父类开始创建。当我们用子类的无参构造函数创建子类对象时,会去先递归调用父类的无参构造方法,这时候如果某个类的父类没有无参构造方法就会出错啦。
(3)参考链接
there is no default constructor available in …

2.9 ither fix the issues identified by lint, or modify your build script as follows

(1)Android studio 在打包时报错如下:

Lint found fatal errors while assembling a release target.

To proceed, either fix the issues identified by lint, or modify your build script as follows:
...
android {
    lintOptions {
        checkReleaseBuilds false
        // Or, if you prefer, you can continue to check for errors in release builds, but continue the build even when errors are found:
        abortOnError false
    }
}

(2)解决方法

// Android Studio里,每个model下都有一个build.gradle。我们需要把如下代码添加到各个model下的build.gradle
android {
    compileSdkVersion 24
    buildToolsVersion '26.0.2'
    
    lintOptions {
        abortOnError false
    }
}

2.10 Could not find com.android.tools.build:aapt2:3.2.0-alpha14-4748712.

(1)问题描述

Could not find com.android.tools.build:aapt2:3.2.0-alpha14-4748712.
Searched in the following locations:
    file:/D:/android_sdk/extras/m2repository/com/android/tools/build/aapt2/3.2.0-alpha14-4748712/aapt2-3.2.0-alpha14-4748712.pom
    file:/D:/android_sdk/extras/m2repository/com/android/tools/build/aapt2/3.2.0-alpha14-4748712/aapt2-3.2.0-alpha14-4748712-windows.jar
    file:/D:/android_sdk/extras/google/m2repository/com/android/tools/build/aapt2/3.2.0-alpha14-4748712/aapt2-3.2.0-alpha14-4748712.pom
    file:/D:/android_sdk/extras/google/m2repository/com/android/tools/build/aapt2/3.2.0-alpha14-4748712/aapt2-3.2.0-alpha14-4748712-windows.jar

(2)在最上级的builde.gralde增加谷歌库 解决问题

allprojects {
    repositories {
        google() // 新增的
        jcenter()
    }
}

2.11 解决android studio "found an invalid color"或Error:Execution failed for task ':app:mergeDebugResources’的问题

引用了一些第三方库及资源,报了一个错误:found an invalid color,发现是.9图片造成的,找了下项目的.9图片,发现有一张并未绘制黑边,然后随便加了一点就编译就通过了。
Android进阶之开发问题笔记_第22张图片
参考链接:解决android studio "found an invalid color"的问题

关于AS用点9图时遇到的错误的解决方法Error:Execution failed for task ‘:app:mergeDebugResources’

2.12 解决Android Studio不停的Indexing的问题

(1)问题分析:启动 Android Studio,不停的Indexing

(2)解决方案

File > Invalidate Caches/Restart

2.13 Android Studio SSL peer shut down incorrectly问题

(1)问题分析:AndroidStudio 编译时出现如下问题 SSL peer shut down incorrectly 或者某些jar包下载不下来,一般是因为墙的原因导致的。这时候我们就需要配置镜像来解决这个问题。
(2)解决方案:(为了提高jar包的下载速度也可以配置)配置的方法就是在根build.gradle中添加镜像仓库,一般我们选择阿里的http://maven.aliyun.com/nexus/content/groups/public/。注意要将jcenter放到最后一个,因为他就是那个下载慢,或者报错的罪魁祸首,

buildscript {

    repositories {
        google()
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
    }
}

allprojects {
    repositories {
        google()
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

(3)参考链接
Android Studio SSL peer shut down incorrectly 问题

2.14 A problem occurred evaluating project ‘:app’. > ASCII

(1)错误信息

FAILURE: Build failed with an exception.
* Where:
Build file 'F:\git\i***\build.gradle' line: 1
* What went wrong:
A problem occurred evaluating project ':**'.
> ASCII
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

(2)原因分析
依赖的Gradle插件内部含有中文,因此报错,所以在开发Gradle插件时不可以使用中文注释

2.15 误删R文件报错:error: failed linking references,无法生成新的R文件

(1)问题报错信息如下:
Android进阶之开发问题笔记_第23张图片Android进阶之开发问题笔记_第24张图片
(2)描述:通过clean,ReBulid无效,重新拉取代码重新运行无效,升级了AS也无效等等。
(3)解决方案1:最后找到了一篇文章error: failed linking references,重点如下截图,解决了此问题。通过clean、ReBulid,重新拉取代码重新运行,升级了AS都无效的原因应该是都使用了缓存的依赖,无法生成新的R文件,需要将.gradle的caches删除,ReBulid后才能生成新R文件。
Android进阶之开发问题笔记_第25张图片
Android进阶之开发问题笔记_第26张图片

2.16 Caused by: com.android.tools.r8.CompilationFailedException: Compilation fail: 异常处理

(1)问题报错信息如下:
Android进阶之开发问题笔记_第27张图片
(2)解决方案1
Android进阶之开发问题笔记_第28张图片
(2)解决方案2
Android进阶之开发问题笔记_第29张图片
(3)解决方案3:将项目根部build以及主module下的build2个文件夹删除,再进行Invalidate Caches/Restart操作,重启AS,再次编译Project。
Android进阶之开发问题笔记_第30张图片
(4)解决方案4
如果是接入sdk出错,可以先git stash恢复代码再运行项目;运行正常后再git stash pop将sdk弹出,继续运行,通常是可以通过的。

(5)解决方案5
参考### 2.15(3)的将.gradle的caches删除。

2.17 android studio 3.4 default activity not found解决办法

(1)出错内容
启动android studio调试工程出现“default activity not found”的报错。
(2)解决办法1
AndroidManifest.xml文件中没有声明相应Activity,所以最好直接用AS快速创建Activity。
(3)解决办法2

1、关闭as
2、找到androidstudio的安装路径(C:\Users\xxx\.AndroidStudio3.2)
3、进入AndroidStudio3.2\system\caches
4、删除里面文件
5、重启as

(4)解决办法3
Android进阶之开发问题笔记_第31张图片
(5)学习链接
android studio 3.2 default activity not found解决办法

2.23 导入鲸鱼项目出现的导入包出错问题

(1)出错截图
Android进阶之开发问题笔记_第32张图片
(2)分析原因:build.gradle(project:-----)

allprojects {
    repositories {
        jcenter()
        maven {
            url "https://maven.google.com"
        }
        google()
        // ........
    }
}

(3)修复问题:将maven-url置于前面,google() 居中,再到 jcenter()

allprojects {
    repositories {
        maven {
            url "https://maven.google.com"
        }
        google() 
        jcenter()
        // ........
    }
}

2.20 ArrayList强引用造成的IllegalStateException

(1)错误信息

# main(1)
java.lang.IllegalStateException
The application's PagerAdapter changed the adapter's contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 1, found: 2147483647 Pager id: com..book:id/viewpager_banner Pager class: class com..SmoothScrollViewPager Problematic adapter: class com.AdBannerView$a

(2)报错的代码

    public void showBanner(List bannerList) {
        mBannerList = bannerList;

        if (mBannerList != null && mBannerList.size() > 0) {
            int size = mBannerList.size();
            mViewPagerBanner.onShow();
        } else {
            mViewPagerBanner.setAdapter(null);
        }
    }
   /**
     * 通知BannerPagerAdapter刷新
     */
    public void notifyDataSetChanged() {
        if (mBannerPagerAdapter != null) {
            mBannerPagerAdapter.notifyDataSetChanged();
        }
    }

(3)修改后的代码

public void showBanner(List bannerList) {
        if (bannerList != null && bannerList.size() > 0) {
            if (mBannerList == null) {
                mBannerList = new ArrayList<>();
            }
            mBannerList.clear();
            mBannerList.addAll(bannerList);
        } else {
            mViewPagerBanner.setAdapter(null);
        }
    }
 /**
     * 通知BannerPagerAdapter刷新
     *
     * @param bannerList
     */
    public void notifyDataSetChanged(List bannerList) {
        if (bannerList != null && bannerList.size() > 0 && mBannerPagerAdapter != null) {
            if (mBannerList == null) {
                mBannerList = new ArrayList<>();
            }
            mBannerList.clear();
            mBannerList.addAll(bannerList);
            mBannerPagerAdapter.notifyDataSetChanged();
        }
    }

(4)原因分析
因为mBannerList强引用持有,当mBannerList的数量改变时,没有及时调用notifyDataSetChanged()报错。但是我需要使用mBannerList内部的对象强引用,所以可以通过mBannerList.addAll(bannerList),内部的对象仍然是强引用。

2.21 RecyclerView的IndexOutOfBoundsException问题

2.21.1 问题描述

# main(1)
java.lang.IndexOutOfBoundsException

Inconsistency detected. Invalid item position 8(offset:8).state:9 
com.chuangyue.baselib.widget.recyclerview.VerticalRecyclerView{d2e3002 VFED..... .F...... 0,0-720,1136}, adapter:com.novel..me.a.d.a@30ab3a2,layout:com.baselib.widget.recyclerview.VerticalRecyclerView$1@f960833, 
context:com.novel..me.ui.activity.RequestResourceActivity@3a5ff45

1 android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5923)
2 android.support.v7.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:286)
3 android.support.v7.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:343)
4 android.support.v7.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:359)
5 android.support.v7.widget.GapWorker.prefetch(GapWorker.java:366)
6 android.support.v7.widget.GapWorker.run(GapWorker.java:397)
7 android.os.Handler.handleCallback(Handler.java:815)
8 android.os.Handler.dispatchMessage(Handler.java:104)
9 android.os.Looper.loop(Looper.java:207)
10 android.app.ActivityThread.main(ActivityThread.java:5856)
11 java.lang.reflect.Method.invoke(Native Method)
12 com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1026)
13 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:887)
# main(2)
java.lang.IndexOutOfBoundsException

Inconsistency detected. Invalid item position 120(offset:120).state:121 com.chuangyue.baselib.widget.recyclerview.VerticalRecyclerView{ac3f757 VFED..... .F...... 0,0-840,1704}, 
 layout:com..widget.recyclerview.VerticalRecyclerView$1@414df2d, 
 
1 android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5923)
2 android.support.v7.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:286)
3 android.support.v7.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:343)
4 android.support.v7.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:359)
5 android.support.v7.widget.GapWorker.prefetch(GapWorker.java:366)
6 android.support.v7.widget.GapWorker.run(GapWorker.java:397)
7 android.os.Handler.handleCallback(Handler.java:790)
8 android.os.Handler.dispatchMessage(Handler.java:99)
9 android.os.Looper.loop(Looper.java:197)
10 android.app.ActivityThread.main(ActivityThread.java:7022)
11 java.lang.reflect.Method.invoke(Native Method)
12 com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:515)
13 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:837)

2.21.2 原因

在滑动过程中,删除或者插入数据,没及时通知RecyclerView更新缓存,mAdapter.getItemCount()获取当前数据数量<缓存的offsetPosition位置,就会抛异常。

final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount() + exceptionLabel());
                }
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount()  + exceptionLabel());
            }
}

2.21.3 解决思路

(1)禁止在滑动时删除、插入数据,尤其注意上拉刷新时removeFootView(这是X一年多RecyclerView没有根治此异常的原因)

private RefreshLayoutFooter mFooter;
private ViewGroup.LayoutParams mFooterParams;
private int mFooterHeight;

优化前:

    /**
     * 设置是否允许上拉加载更多
     *
     * @param removeView 是否remove FooterView
     */
    private void setLoadMoreEnable(boolean enable, boolean removeView) {
        mIsEnableLoadMore = enable;
        if (!mIsEnableLoadMore) {
            if (mIsAddedFooter && removeView && mRecyclerView != null) {
                mRecyclerView.removeFootView(mFooter);
                mIsAddedFooter = false;
            }
        } else {
            if (!mIsAddedFooter && mRecyclerView != null) {
                mRecyclerView.addFootView(mFooter);
                mIsAddedFooter = true;
            }
        }
    }

优化后:

    /**
     * 设置是否允许上拉加载更多
     *
     * @param removeView 是否remove FooterView
     */
    private void setLoadMoreEnable(boolean enable, boolean removeView) {
        mIsEnableLoadMore = enable;
        if (!mIsEnableLoadMore) {
            if (mIsAddedFooter && removeView && mRecyclerView != null) {
                // removeFootView()会引起IndexOutOfBoundsException异常
                mFooterParams.height = 0;
                mFooter.setLayoutParams(mFooterParams);
                mIsAddedFooter = false;
            }
        } else {
            if (!mIsAddedFooter && mRecyclerView != null) {
                mFooterParams.height = mFooterHeight;
                mFooter.setLayoutParams(mFooterParams);
                mIsAddedFooter = true;
            }
        }
    }

(2)数据源,删除,增加时,notify及时同步
推荐1:

public void addItems(List items, boolean isClearData) {
        if (mRequestItems == null) {
            mRequestItems = new ArrayList<>();
        }

        if (isClearData) {
            mRequestItems.clear();
            notifyDataSetChanged();
        }

        mRequestItems.addAll(items);
        notifyDataSetChanged();
    }

推荐2:

public void setItems(ArrayList
newArticles) { // 获取刷新前数据长度 int oldSize = articles.size(); // 删除当前所有数据 articles.clear(); // 告诉回收器,所有的旧数据都不见了 adapter.notifyItemRangeRemoved(0, oldSize); // 添加新数据 articles.addAll(newArticles); // 告诉回收器,有多少新数据要插入 adapter.notifyItemRangeInserted(0, newArticles.size()); }

(3)刷新数据时禁止滑动,例如:弹出对话框禁止滑动、设置不可触摸、在数据刷新完毕后才操作置顶

①设置不可触摸:

private boolean mIsRefreshing=false;
mRecyclerView.setOnTouchListener(
    new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        if (mIsRefreshing) {
          return true;
        } else {
          return false;
        }
      }
    }
);
// 当刷新时设置
// mIsRefreshing=true;
// 刷新完毕后还原为false
// mIsRefreshing=false;

②在数据刷新完毕后才操作置顶:

// 在拉取新数据前置顶,报错
private void getTopList(boolean isLoadMore) {
    if (!isLoadMore && mRefreshLayout != null) {
        mRefreshLayout.scrollToPosition(0);
    }

    if (getPresenter() != null) {
        getPresenter().getTopList(mCurTopListId, isLoadMore);
    }
}
// 在拉取新数据后,更新列表后,再操作置顶
@Override
public void updateAdapter(List datas, boolean isClearData) {
	if (getPresenter() != null && mAdapter != null) {
    	mAdapter.addItems(getPresenter().setItems(datas, isClearData), isClearData);
        mAdapter.notifyDataSetChanged();

        // 处理切换左侧子榜单时列表未滑动到顶部的bug
        if (isClearData && mRefreshLayout != null) {
        	VerticalRecyclerView recyclerView = mRefreshLayout.getRecyclerView();
            if (recyclerView != null && recyclerView.canScrollVertically(-1)) {
           		recyclerView.scrollToPosition(0);
            }
        }
    }
}

(4)继承LinearLayoutManager,重写onLayoutChildren并try-catch异常

public class WrapContentLinearLayoutManager extends LinearLayoutManager {
    public WrapContentLinearLayoutManager(Context context) {
        super(context);
    }

    public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        try {
            super.onLayoutChildren(recycler, state);
        } catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
        }
    }
}

mRecyclerView.setLayoutManager(new WrapContentLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));

2.21.4 学习链接

RecyclerView系列(4)—XRexyclerView的坑,java.lang.IndexOutOfBoundsException: Inconsistency detected

RecyclerView和java.lang.IndexOutOfBoundsException:检测到不一致。三星设备中无效的视图支架适配器positionViewHolder

Android 解决java.lang.IndexOutOfBoundsException: Inconsistency detected错误

2.22 EditText.setSelection()的IndexOutOfBoundsException: setSpan (14 … 14) ends beyond length 9

Android进阶之开发问题笔记_第33张图片
(1)原因1:设置了filter,设置最大的字数限制为14

private static final int MAX_INPUT_COUNT = 14;
editText.setFilters(new EditTextInputFilter[]{new EditTextInputFilter.LengthFilter(MAX_INPUT_COUNT)});

(2)第一步解决:当字符串的长度超过了MAX_INPUT_COUNT,截取0—MAX_INPUT_COUNT

if (!TextUtils.isEmpty(content)) {
	 if (content.length() > MAX_INPUT_COUNT) {
     	  content = content.substring(0, MAX_INPUT_COUNT);
     }
            
     mEtContent.setText(content);
     mEtContent.setSelection(content.length());
}

(3)结果:当输入content = “ddddd若干单价金额绝对金”,还是出现此异常。备注:自定义的EditTextInputFilter限制字母长度为1汉字为2计算。

(4)原因2:content.length()==14,但是Selection的位置经过EditTextInputFilter过滤后按照字母长度为1汉字为2计算,结果是:5 + 9*2 = 23。

(5)第二步解决:

    private void dealEditSelection(String content) {
        if (!TextUtils.isEmpty(content)) {
            StringBuilder subBuilder = new StringBuilder();
            int destLen = 0;
            for (int i = 0; i < content.length(); i++) {
                String subStr = content.substring(i, i + 1);
                destLen += subStr.getBytes().length > 1 ? 2 : 1;

                if (destLen <= MAX_INPUT_COUNT) {
                    subBuilder.append(subStr);
                } else {
                    break;
                }
            }

            String destContent = subBuilder.toString();
            mEtContent.setText(destContent);
            mEtContent.setSelection(destContent.length());
        }
    }

(6)参考链接
记一次EditText设置默认选中setSelection的一个bug

2.23 Android P 使用HttpUrlConnection进行http请求会出现以下异常

(1)前言为保证用户数据和设备的安全,Google针对下一代 Android 系统(Android P) 的应用程序,将要求默认使用加密连接,这意味着 Android P 将禁止 App 使用所有未加密的连接,因此运行 Android P 系统的安卓设备无论是接收或者发送流量,未来都不能明码传输,需要使用下一代(Transport Layer Security)传输层安全协议,而 Android Nougat 和 Oreo 则不受影响。
(2)问题
因此在Android P 使用HttpUrlConnection进行http请求会出现以下异常

 W/System.err: java.io.IOException: Cleartext HTTP traffic to **** not permitted

使用OKHttp请求则出现

java.net.UnknownServiceException: CLEARTEXT communication ** not permitted by network security policy

(3)分析
在Android P系统的设备上,如果应用使用的是非加密的明文流量的http网络请求,则会导致该应用无法进行网络请求,https则不会受影响,同样地,如果应用嵌套了webview,webview也只能使用https请求。
(4)解决方法
①APP改用https请求
②targetSdkVersion 降到27以下
③更改网络安全配置

// 首先,在res文件夹下创建一个xml文件夹,然后创建一个network_security_config.xml文件,文件内容如下:


    

// 接着,在AndroidManifest.xml文件下的application标签增加以下属性:

④在AndroidManifest.xml配置文件的application标签中直接插入

android:usesCleartextTraffic="true"

(5)学习链接
Android高版本联网失败报错:Cleartext HTTP traffic to xxx not permitted解决方法

2.24 版本问题遇到的 Caused by: java.lang.IllegalArgumentException

版本问题遇到的 Caused by: java.lang.IllegalArgumentException

2.25 RxJava2取消订阅后,抛出的异常无法捕获,导致程序崩溃

(1)报错日志
io.reactivex.exceptions.UndeliverableException: The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with

(2)解决方法

public class ReaderApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // 在Application中设置Java全局异常监控
        RxJavaPlugins.setErrorHandler(Throwable::printStackTrace);
    }
}

(3)参考链接
io.reactivex.exceptions.UndeliverableException: The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with.

2.26 java.lang.IllegalAccessError非法访问错误

(1)错误

java.lang.IllegalAccessError: Field 'com.lm.presenter.LmViewPresenter.view' is inaccessible to class 'com.lm.presenter.SplashPresenter' (declaration of 'com.lm.presenter.SplashPresenter' appears in /data/data/com.luomi.sdkdemo/cache/wand/hotfix_pack_7.dex)
       at com.lm.presenter.SplashPresenter.addAndSetImageView(SplashPresenter.java:291)
       at com.lm.presenter.SplashPresenter.access$500(SplashPresenter.java:47)
       at com.lm.presenter.SplashPresenter$3.run(SplashPresenter.java:172)
       at android.os.Handler.handleCallback(Handler.java:793)
       at android.os.Handler.dispatchMessage(Handler.java:98)
       at android.os.Looper.loop(Looper.java:176)
       at android.app.ActivityThread.main(ActivityThread.java:6701)
       at java.lang.reflect.Method.invoke(Native Method)
       at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:249)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)

(2)解决
①集成MSA的sdk在module的libs下报错,将sdk迁移到主工程的libs下解决了问题。

②原因是自定义的classloader加载的class中能找到这个类,但这个类自定义的不存在与我的classloader加载的dex中,而是在别的classloader中。因此把这个类放入dex中解决了问题。

(3)参考链接
记一次java.lang.IllegalAccessError错误

2.27 java.lang.NoSuchMethodError找不到类错误

(1)错误

Pending exception java.lang.NoSuchMethodError: no static or non-static method 
"Lcom/tencent/smtt/export/external/libwebp;.nativeGetInfo([B[I[I)I"

(2)解决
在混淆的时候也应该对新增/更新的模块进行-keep。

(3)参考链接
java.lang.NoSuchMethodError找不到类错误

2.28 UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/项目包名

(1)错误

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.gcloud.jlgc-1/base.apk"],
nativeLibraryDirectories=[/data/app/com.gcloud.jlgc-1/lib/arm, /data/app/com.gcloud.jlgc-1/base.apk!/lib/armeabi-v7a, 
/vendor/lib, /system/lib]]] couldn't find "libbdSpilWakeup.so"

(2)解决
在main下面新建jniLibs,并把.so文件复制到该目录下,并把jar放在lib目录下并添加到项目中

android {
	defaultConfig {
        ndk {
            // 设置支持的SO库架构
            abiFilters 'armeabi' ,'x86', 'armeabi-v7a', 'x86_64','arm64-v8a'
        }
    }
    
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }    
}

(3)参考链接
UnsatisfiedLinkError: dalvik.system.PathClassLoader

2.29 java.lang.IllegalStateException:The specified child already has a parent

(1)错误

java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

(2)解决方案1:帮孩子找父母,然后断绝亲子关系

// 找出parent(直接parent.removeView不就可以了嘛,可是你会发现ViewParent没有removeView方法)
ViewParent parent = childView.getParent();
// 找出这个parent具体是什么类型,例如:FrameLayout
if(parent!=null)
    Log.i("who_are_you",parent.getClass().toString());
}
// 直接强转,解绑
FrameLayout mFrameLayout = (FrameLayout)parent;
mFrameLayout.removeView(child_view);

(3)解决方案2:给孩子找个继父母,然后断绝亲子关系

if(childView.getParent() != null) {
	((ViewGroup)childView.getParent()).removeView(childView);
}

(3)学习链接
java.lang.IllegalStateException:The specified child already has a parent异常万能解决方案:removeView

2.30 Androidx的报错

2.30.1 使用androidx时Program type already present报错的一种解决尝试

(1)错误
如果在使用androidx库时, 又不小心间接使用了其他的老库, 可能会遇到如下的报错:

Program type already present: android.support.v4.os.ResultReceiver
Error: Program type already present: android.support.v4.app.INotificationSideChannel
Error: Program type already present: xxxxxxxx(此处可能会有各种变形)

(2)解决方案
可以尝试在gradle.properties中添加:

android.useAndroidX=true
android.enableJetifier=true

(3)学习链接
使用androidx时Program type already present报错的一种解决尝试

2.30.2 编译失败:More than one file was found with OS independent path 'META-INF/proguard/androidx

(1)错误

Execution failed for task “:app:transformResourcesWithMergeJavaResForDebug”.
More than one file was found with OS independent path ‘META-INF/proguard/androidx-annotations.pro’

(2)解决方案
在build.gradle文件的android节点下增加packagingOptions,过滤掉提示重复的文件:

packagingOptions {
	exclude 'META-INF/proguard/androidx-annotations.pro'
}

Android进阶之开发问题笔记_第34张图片
(3)学习链接
Android Studio编译失败:More than one file was found with OS independent path 'META-INF/proguard/androidx

2.31 引入了多个三方库.在调用的时候会出现版本对应不上,引起的问题:java.lang.NoClassDefFoundError: Failed resolution of: Landroid/support/v4/animation/AnimatorCompatHelper

(1)错误
java.lang.NoClassDefFoundError: Failed resolution of: Landroid/support/v4/animation/AnimatorCompatHelper

(2)原因
经过查找,项目中使用的RecycleView类,进入类里面发现AnimatorCompatHelper缺少引用,使用命令gradlew :app:dependencies查看库的依赖树发现RecycleView使用的是25.1.0,而其它support使用的是26.0.1,而导致无法发现AnimatorCompatHelper。

(3)解决方案
①删除其中一个recyclerview的引用;
②都改为版本一致的ecyclerview的引用;
③build.gradle里面加入如下代码块

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (!requested.name.startsWith("multidex")) {
                details.useVersion '26.0.1'
            }
        }
    }
}

(4)学习链接
java.lang.NoClassDefFoundError: Failed resolution of: Landroid/support/v4/animation/AnimatorCompatHelper

java.lang.NoClassDefFoundError: Failed resolution of: Landroid/support/v4/animation/AnimatorCompatHelper

3 工具类集合

3.1 正则表达式匹配URL

(1) 判断是否是完整的域名

public static boolean isCompleteUrl(String text) {
        Pattern p = Pattern.compile("((http|ftp|https)://)(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?", Pattern.CASE_INSENSITIVE);
        Matcher matcher = p.matcher(text);
        return matcher.find();
    }

(2)判断是否是缺少前缀的域名

/**
     * 是否是缺少前缀的域名
     */
    public static boolean isHalfCompleteUrl(String text) {
        Pattern p = Pattern.compile("(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?", Pattern.CASE_INSENSITIVE);
        Matcher matcher = p.matcher(text);
        return matcher.find();
    }

(3)参考链接
Android利用正则表达式如何匹配URL

Android正则表达式匹配URL

3.2 时间格式化

(1)日期格式化

 /**
     * 日期格式化
     * @param date
     * @return
     */
    public static String formatDate(Long date) {
        SimpleDateFormat format = new SimpleDateFormat("MM月dd日", Locale.getDefault());// yyyy-MM-dd HH:mm:ss
        return format.format(new Date(date));
    }

android 时间获取以及时间格式化

(2)获取系统时间

SimpleDateFormat formatter = new SimpleDateFormat ("yyyy年MM月dd日 HH:mm:ss ");  
Date curDate = new Date(System.currentTimeMillis());//获取当前时间  
String str = formatter.format(curDate);  

Android–获取当前系统时间

(3)比较两个日期是否在同一天

  /**
     * 比较两个日期是否在同一天
     * @param date1
     * @param date2
     * @return
     */
    public static boolean isSameDate(Long date1, Long date2) {
        Calendar cal1 = Calendar.getInstance();
        cal1.setTimeInMillis(date1);
        Calendar cal2 = Calendar.getInstance();
        cal2.setTimeInMillis(date2);
        boolean isSameYear = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR);
        boolean isSameMonth = isSameYear && cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH);
        boolean isSameDate = isSameMonth && cal1.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH);
        return isSameDate;
    }

(4)判断两天在同一周内

/** 
 * 判断date和当前日期是否在同一周内(开始是周日,结束是周六 )
 * 注: 参考(2)获取系统时间先获取系统时间,并转化为Date
 * 
 * Calendar类提供了一个获取日期在所属年份中是第几周的方法,对于上一年末的某一天 
 * 和新年初的某一天在同一周内也一样可以处理,例如2012-12-31和2013-01-01虽然在 
 * 不同的年份中,但是使用此方法依然判断二者属于同一周内 
 */  
public static boolean isSameWeekWithToday(Date date) {      
    if (date == null) {  
        return false;  
    }  
      
    // 0.先把Date类型的对象转换Calendar类型的对象  
    Calendar todayCal = Calendar.getInstance();  
    Calendar dateCal = Calendar.getInstance();  
  
    todayCal.setTime(new Date());  
    dateCal.setTime(date);  
  
    // 1.比较当前日期在年份中的周数是否相同  
    if (todayCal.get(Calendar.WEEK_OF_YEAR) == dateCal.get(Calendar.WEEK_OF_YEAR)) {  
        return true;  
    } else {  
        return false;  
    }  
}  

3.3 判断是否包含SIM卡

/**
 * 判断是否包含SIM卡
 */
public static boolean hasSimCard() {
    TelephonyManager telMgr = (TelephonyManager)
    FungoApplication.getAppContext().getSystemService(Context.TELEPHONY_SERVICE);
    boolean result = true;
    switch (telMgr.getSimState()) {
        case TelephonyManager.SIM_STATE_ABSENT:
            // 没有SIM卡
            result = false;
            break;
        case TelephonyManager.SIM_STATE_UNKNOWN:
            String subscriberId = telMgr.getSubscriberId();
            result = !TextUtils.isEmpty(subscriberId);
            break;
        default:
            break;
    }
    return result;
}

如何通过代码判断手机中是否有SIM卡及各种状态

Android判断手机里是否有SIM卡

3.4 去掉空格

  /**
     * 匹配任何不可见字符,包括空格、制表符、换页符、回车符等,[\s]等价于[\f\n\r\t\v]
     * 注意:这个已经包含\n \t 不要再去附加\n \t的情况
     *
     * @param s
     */
    public static String removeSpace(String s) {
        String regex = "\\s";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(s);
        return matcher.replaceAll("");
    }

Android开发之去掉空格

3.5 将手机号中间隐藏为星号(*)

// 截取手机号码 方法一
String phonenum = "15718807588";
    if(!TextUtils.isEmpty(phonenum) && phonenum.length() > 6) {
    StringBuilder sb  =new StringBuilder();
    for (int i = 0; i < phonenum.length(); i++) {
        char c = phonenum.charAt(i);
        if (i >= 3 && i <= 6) {
            sb.append('*');
        } else {
            sb.append(c);
        }
    }
    ToastUtil.showImgMessage(context,sb.toString()+"1");
}
// 截取手机号码 方法二
phonenum = phonenum.substring(0, 3) + "****" + phonenum.substring(7, 11);
ToastUtil.showImgMessage(context,phonenum + "2");
// 方法三:用****替换手机号码中间4位
String mobile = "15718807588";
String maskNumber = mobile.substring(0,3)+"****"+mobile.substring(7,mobile.length());

4 assets存储原始文件

assets下文件在打包生成apk的时候不会被编译,以文件原有的方式来保存,可以通过AssetManager来操作这些文件。避免了存储在res文件下的文件模糊、压缩等问题。
  系统在编译的时候不会编译assets下的资源文件,所以我们不能通过R.xxx.ID的方式访问它们。那我么能不能通过该资源的绝对路径去访问它们呢?因为apk安装之后会放在/data/app/**.apk目录下,以apk形式存在,asset/res和被绑定在apk里,并不会解压到/data/data/YourApp目录下去,所以我们无法直接获取到assets的绝对路径,因为它们根本就没有。

4.1 加载assets目录下图片或网页

这里写图片描述

ImageLoader.getInstance().displayImage(Uri.parse("file:///android_asset/ic_qrcode_default.png"), ivQrCode, ImageLoaderOptions.getOptionQrCode());

4.2 AssetManager类

(1)访问assets目录下的资源文件
  AssetManager.open(String filename),返回的是一个InputSteam类型的字节流,这里的filename必须是文件比如(aa.txt;img/semll.jpg),而不能是文件夹。

AssetManager assetManager = getResources().getAssets();
InputStream inputStream = assetManager.open("fungo_cst_src.txt");

(2)获取assets的文件及目录名

// 获取assets目录下的所有文件及目录名,content
//(当前的上下文如Activity,Service等ContextWrapper的子类的都可以)
String fileNames[] =context.getAssets().list(path);    

4.3 asset目录与res目录的区别

(1)res 目录下面有很多文件,例如 drawable,mipmap,raw 等。res 下面除了 raw 文件不会被压缩外,其余文件都会被压缩。同时 res目录下的文件可以通过R 文件访问。
(2)asset 也是用来存储资源,但是 asset 文件内容只能通过路径或者 AssetManager 读取。

5 字符串格式化-String.format()的使用

5.1 format()方法

(1)String类的format()方法用于创建格式化的字符串以及连接多个字符串对象。熟悉C语言的同学应该记得c语言的sprintf()方法,两者有类似之处。format()方法有两种重载形式。
(2)format(String format, Object… args) 新字符串使用本地语言环境,制定字符串格式和参数生成格式化的新字符串。
(3)format(Locale locale, String format, Object… args) 使用指定的语言环境,制定字符串格式和参数生成格式化的字符串。

5.2 常规类型的格式化

(1)常规类型
Android进阶之开发问题笔记_第35张图片
(2)测试用例

public static void main(String[] args) {  
    String str=null;  
    str=String.format("Hi,%s", "王力");  
    System.out.println(str);  
    str=String.format("Hi,%s:%s.%s", "王南","王力","王张");            
    System.out.println(str);                           
    System.out.printf("字母a的大写是:%c %n", 'A');  
    System.out.printf("3>7的结果是:%b %n", 3>7);  
    System.out.printf("100的一半是:%d %n", 100/2);  
    System.out.printf("100的16进制数是:%x %n", 100);  
    System.out.printf("100的8进制数是:%o %n", 100);  
    System.out.printf("50元的书打8.5折扣是:%f 元%n", 50*0.85);  
    System.out.printf("上面价格的16进制数是:%a %n", 50*0.85);  
    System.out.printf("上面价格的指数表示:%e %n", 50*0.85);  
    System.out.printf("上面价格的指数和浮点数结果的长度较短的是:%g %n", 50*0.85);  
    System.out.printf("上面的折扣是%d%% %n", 85);  
    System.out.printf("字母A的散列码是:%h %n", 'A');

(3)结果

Hi,王力  
Hi,王南:王力.王张  
字母a的大写是:A   
3>7的结果是:false   
100的一半是:50   
100的16进制数是:64   
100的8进制数是:144   
50元的书打8.5折扣是:42.500000 元  
上面价格的16进制数是:0x1.54p5   
上面价格的指数表示:4.250000e+01   
上面价格的指数和浮点数结果的长度较短的是:42.5000   
上面的折扣是85%   
字母A的散列码是:41   

5.3 api-getString()方法

发送 %d/9

mTvSend.setText(getString(R.string.local_document_send_limit, selectedDocuments.size()));

5.4 其他参考链接

JAVA字符串格式化-String.format()的使用

6 热键监听

6.1 监听home键的实现方式

以前写的那些方式都不是很好用。现在的这种方式通过广播的方式监听home键:
(1)首先是创建一个广播接受者;
(2)注册监听;
(3)完整代码:

public class MainActivity extends Activity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        //注册广播  
        registerReceiver(mHomeKeyEventReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));  
    }  
  
    /** 
     * 监听是否点击了home键将客户端推到后台 
     */  
    private BroadcastReceiver mHomeKeyEventReceiver = new BroadcastReceiver() {  
        String SYSTEM_REASON = "reason";  
        String SYSTEM_HOME_KEY = "homekey";  
        String SYSTEM_HOME_KEY_LONG = "recentapps";  
           
        @Override  
        public void onReceive(Context context, Intent intent) {  
            String action = intent.getAction();  
            if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {  
                String reason = intent.getStringExtra(SYSTEM_REASON);  
                if (TextUtils.equals(reason, SYSTEM_HOME_KEY)) {  
                     //表示按了home键,程序到了后台  
                } else if(TextUtils.equals(reason, SYSTEM_HOME_KEY_LONG)){  
                    //表示长按home键,显示最近使用的程序列表  
                }  
            }   
        }  
    };  
}  

6.2 返回键的监听及处理

/**  
 * Demo描述: 处理Back键按下事件  
 * 注意事项: 以下两种方法勿一起使用  
 */    
public class MainActivity extends Activity {    
    
    @Override    
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);    
        setContentView(R.layout.main);    
    }    
    /**  
     * 方法1: 监听Back键按下事件
     * 注意: super.onBackPressed()会自动调用finish()方法,关闭当前Activity. 若要屏蔽Back键盘,注释该行代码即可  
     */    
    @Override    
    public void onBackPressed() {    
        super.onBackPressed();    
        System.out.println("按下了back键   onBackPressed()");           
    }    
          
   /**  
    * 方法2: 监听Back键按下事件  
    * 注意: 返回值表示:是否能完全处理该事件在此处返回false,所以会继续传播该事件.在具体项目中此处的返回值视情况而定.  
    */    
    @Override    
    public boolean onKeyDown(int keyCode, KeyEvent event) {    
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {    
             System.out.println("按下了back键   onKeyDown()");     
             return false;    
        }else {    
            return super.onKeyDown(keyCode, event);    
        }    
    }    
}    

8 安卓中加载布局文件的三种方法

(1)方法1

LinearLayout layout=(LinearLayout)LayoutInflater.from(getApplicationContext()).inflate(R.layout.list_view_item_text, null);
LinearLayout layout=(LinearLayout)LayoutInflater.from(MainActivity.this).inflate(R.layout.list_view_item_text, null);

(2)方法2

LayoutInflater layoutInflater=(LayoutInflater) getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout=(LinearLayout)layoutInflater.inflate(R.layout.list_view_item_text, null);

(3)方法3

LinearLayout layout=(LinearLayout)getLayoutInflater().inflate(R.layout.list_view_item_text, null);
LinearLayout layout=(LinearLayout)MainActivity.this.getLayoutInflater().inflate(R.layout.list_view_item_text, null);

9 singleTop或者singleTask的onNewIntent调用时机

启动模式为singleTop时,系统会先检查栈顶是不是该Activity,如果不是的话,它会创建一个该Activity的实例,并启动onCreate函数。如果栈顶是,则不会再创建该Activity,不会执行onCreate函数,而是执行onNewIntent函数来重新启动。同理,当启动模式为singleTask时,若栈顶不是该Activity系统会在栈中寻找是否存在这个实例,如是的话就会把这个实例放在栈顶,并把它之前的实例清除掉,就会执行onNewIntent函数(道理和singleTop一样)。两种情况执行的顺序变为:onPause()–>onStop()----发送Intent—>onNewIntent()–>onRestart()–>onStart()–>onResume()。如代码所示:

 public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.main);

   getIntent();// 获取的是第一次启动Activity传递的值
 }

 protected void onNewIntent(Intent intent) {
   super.onNewIntent(intent);

	intent.getExtra().get---//获取新传递的值
 }

12 在子线程中更新UI的解决方法

(1)Activity.runOnUiThread(Runnable )

 runOnUiThread(new Runnable() {
        @Override
        public void run() {
             Toast.makeText(MainActivity.this, "hah", Toast.LENGTH_SHORT).show();
        }
});

(2)子线程调用Handler的sendMessage(message)发送事件

//主线程
Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //操作界面
            myText.setText( 来自网络的信息);
            super.handleMessage(msg);
        }
    };
//子线程
public class MyThread extends Thread {
        public void run() {
            // 耗时操作
            loadNetWork();
            Message msg = new Message();
            mHandler.sendMessage(msg);//向Handler发送消息,
        }
}
//子线程
public class MyThread extends Thread {

		//Looper.getMainLooper()就表示放到主UI线程去处理
		Handler mHandler = new Handler(Looper.getMainLooper()) {
	        @Override
	        public void handleMessage(Message msg) {
	            //操作界面
	            myText.setText( 来自网络的信息);
	            super.handleMessage(msg);
	        }
	    };
      
        public void run() {
            // 耗时操作
            loadNetWork();
            Message msg = new Message();
            mHandler.sendMessage(msg);//向Handler发送消息,
        }
}

(3)View.post(Runnable r):从Runnable派生你的子类,重载run()方法。然后调用View.post(myRunnableObj)即可把你的Runnable对象增加到UI线程中运行。

public void onClick( View v ) {  
        new Thread( new Runnable() {  
                public void run() {  
                       // 耗时操作  
​    ​    ​    ​    ​    ​   loadNetWork();  
​                       myText.( new Runnable() {  
	                       myText.setText( 来自网络的信息);  
                       });  
                 }  
        }).start();  
}

(4)使用异步任务

//UI线程中执行  
new DownloadImageTask().execute("https://www.baidu.com");  

private class DownloadImageTask extends AsyncTask {  
    protected String doInBackground( String... url ) { 
         //后台耗时操作  
         return loadDataFormNetwork( url[0] ); 
    }  

    protected void onPostExecute( String result ) {  
          //得到来自网络的信息刷新页面  
          myText.setText( result );  
   }  
}

(5)RxJava切换子线程到主线程

14 隐式启动Activity

14.1 通过URI或者配置启动Activity

(1)跳转的方法

public static void actionTo(Context context, String name, String url) {
	String url = Uri.parse("svmplayer://player?name=" + name + "&url=" + url));
	openThirdActivity(context, url, "android.intent.category.DEFAULT");
}

14.2 隐式跳转,在发出Intent之前必须通过resolveActivity检查

/**
     * 跳转第三方应用
     * 通过隐式Intent的跳转,在发出Intent之前必须通过resolveActivity检查,避免找不到合适的调用组件,造成ActivityNotFoundException的异常
     *
     * @param context
     * @param url
     */
    public static boolean openThirdActivity(Context context, String url, int name) {
        if (context == null || TextUtils.isEmpty(url)) {
            return false;
        }

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        if (name == THIRD_ACTIVITY_BROWSABLE) {
            intent.addCategory(Intent.CATEGORY_BROWSABLE);
        }
        if (context.getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
            try {
                context.startActivity(intent);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            if (name == THIRD_ACTIVITY_BROWSABLE) {
                ToastUtils.showToast(context, context.getString(R.string.toast_browser_uninstalled));
            } else if (name == THIRD_ACTIVITY_WEIXIN) {
                ToastUtils.showToast(context, context.getString(R.string.toast_weixin_uninstalled));
            }
        }

        return false;
    }

14.3 接收处理方法

try {
        if (intent != null) {
            videoUrl = URLDecoder.decode(intent.getData().getQueryParameter("url"), "UTF-8");
            title = URLDecoder.decode(intent.getData().getQueryParameter("name"), "UTF-8");
        }
    } catch (Exception e) {
        Logger.e(e);
    }

14.4 注册配置


            
                

                
                

                
            
        

14.5 使用Scheme实现从网页启动APP

Android进阶之使用Scheme实现从网页启动APP

15 Java与JS互相调用

15.1 Java调JS(客户端传数据到H5)

(1)首先是JS的一段代码

function javaCallJs(key, value){
	document.getElementById("value").innerHTML = ("欢迎:" + value);
}

(2)然后是在java中调用JS中的方法

public abstract class InvokeJsBase {
    private static final String rs = "javascript:javaCallJs(\"%s\", %s)";

    public String invokeJs() {
        String js = String.format(rs, key(), value());
        return js;
    }

    abstract String key();
    abstract String value();
}
import com.alibaba.fastjson.JSON;

/**
 * 具体实现调用的方法
 */
public class InvokeCollectStatus extends InvokeJsBase {

    public boolean value;

    public InvokeCollectStatus(boolean value) {
        this.value = value;
    }

    @Override
    String key() {
        return "statusKey";
    }

    @Override
    String value() {
        return JSON.toJSONString(this);
    }
}

(3)以上代码就是调用了JS中一个叫javaCallJs(arg)的方法,并传入了一个key、value参数。

15.2 JS调Java(H5传数据到客户端)

(1)配置Javascript接口

webView.addJavascriptInterface(new JSInterface (),"playerClient");

(2)实现Javascript接口类

public class ProgressWebView extends WebView {

    private Context context;
    
    public ProgressWebView(final Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;

        addJavascriptInterface(new JavaScriptinterface(), "playerClient");
    }

    class JavaScriptinterface {
        @JavascriptInterface
        public void showToast(String arg) {
            Toast.makeText(MainActivity.this, arg, Toast.LENGTH_SHORT).show();
        }
    }
}

(3)JS中调用java代码


(4)window.playerClient.showToast(‘JS中传来的参数’)”中的"playerClient"在addJavascriptInterface()中指定的,并且JS向java传递了参数,类型为String。而showToast(String arg)会以Toast的形式弹出此参数。

15.3 WebView详解与简单实现Android与H5互调

WebView详解与简单实现Android与H5互调

Android和H5之间的交互

Hybrid混合开发

18 获取本地视频文件以及缩略图

(1)工具类:参考超级影音或者云图手机电视的本地视频例子。
  
(2)参考例子
RxJava获取本地视频

android 获取本地视频文件以及缩略图

19 自定义SeekBar主题

自定义SeekBar主题

一个功能强大的自定义SeekBar

20 Android自定义星星评分控件

Android自定义星星评分控件,高效

Android进阶之开发问题笔记_第36张图片

21 支持文件(apk)下载

21.1 APP检测安装打开APK

(1)检查APP是否已经安装

    public static boolean isPkgInstalled(Context context, String pkgName) {
        PackageManager packageManager = context.getPackageManager();
        // 获取手机系统的所有APP包名,然后进行一一比较
        List pinfo = packageManager.getInstalledPackages(0);
        for (int i = 0; i < pinfo.size(); i++) {
            if (pinfo.get(i).packageName.equalsIgnoreCase(pkgName))
                return true;
        }
        return false;
    }

(2)安装已经下载好的apk

private void install(String apkname) {
	File dir = mActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
    if (dir != null) {
        String path = dir.getPath() + "/xiaoai.apk";
        File file = new File(path);
        if(file.exists()) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.fromFile(new File(path)),"application/vnd.android.package-archive");
            mActivity.startActivity(intent);
        } else {
            downApk();//安装包已经删除请重新下载
        }
    } else {
        downApk();
    }
}

(3)安装已经下载好的apk

private void openApk(String url) {
	PackageManager packageManager = mActivity.getPackageManager();
    Intent intent = packageManager.getLaunchIntentForPackage("com.fungo.loveshow");
    startActivity(intent);
}

(4)打开另一个APP指定的Activity

 /**
     * 注意:
     * 1.需要将目标Activity的android:exported="true"属性在所属应用AndroidMainfest里设置为true,
     * 意思是当前Activity可以被外部应用访问。
     * 2.需要在当前应用的AndroidMainfest里也声明目标Activity。
     */
    public void exportActivity() {
        Intent intent = new Intent();
        //第一种方式
        ComponentName cn = new ComponentName("com.example.fm", "com.example.fm.MainFragmentActivity");
        intent.setComponent(cn);
        //第二种方式
        //intent.setClassName("com.example.fm", "com.example.fm.MainFragmentActivity");
        intent.putExtra("test", "intent1");
        startActivity(intent);
    }

(5)学习链接
Android APP检测安装打开APK三步操作

Android通过App启动另一个APP

21.2 实现DownloadManager下载apk,并自动提示安装

实现 DownloadManager 下载完 apk 自动提示安装的功能

21.3 让Android WebView支持文件下载

让WebView支持下载,需要给WebView设置下载监听器 setDownloadListener,DownloadListener里面只有一个方法 onDownloadStart,每当有文件需要下载时,该方法就会被回调,下载的 URL 通过方法参数传递,我们可以在这里处理下载事件。

mWebView.setDownloadListener(new DownloadListener() {
    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
        // 处理下载事件
    }
});

(1)跳转浏览器下载

mWebView.setDownloadListener(new DownloadListener() {
    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
        // 处理下载事件-使用14 隐式启动Activity
        openThirdActivity(context, url, Intent.CATEGORY_BROWSABLE);
    }
});

(2)使用系统的DownloadManager下载服务
(3)自定义下载任务
(4)三种方式让 Android WebView 支持文件下载

23 低版本SDK实现高版本api

(1)一般很多高版本的新的API都会在兼容包里面找到替代的实现。比如:Notification,在v4兼容包里面有NotificationCompat类。5.0+出现的backgroundTint,minSdk小于5.0的话会包检测错误,v4兼容包DrawableCompat类。
(2)在高版本 SDK 中使用高版本 API,低版本 SDK 中自己实现。

// 在使用了高版本API的方法前面加一个@TargetApi(API版本号)。
// 在代码中判断版本号来控制不同的版本使用不同的代码。
@TargetApi(11) 
public void text() { 
if(Build.VERSION.SDK_INT >= 11){ 
	// 使用 API 11 的方法
} else {
	// 使用自己实现的方法
}

24 FastJSON实现Map/Json/String互转

(1)用途-举例:将List转为json存储在本地,再取出转化为List使用

(2)主要方法

// 把JSON文本parse为JSONObject或者JSONArray
public static final Object parse(String text);
// 把JSON文本parse成JSONObject  
public static final JSONObject parseObject(String text);
// 把JSON文本parse为JavaBean     
public static final  T parseObject(String text, Class clazz); 
// 把JSON文本parse成JSONArray 
public static final JSONArray parseArray(String text); 
 //把JSON文本parse成JavaBean集合 
public static final  List parseArray(String text, Class clazz);
// 将JavaBean序列化为JSON文本
public static final String toJSONString(Object object);  
// 将JavaBean序列化为带格式的JSON文本 
public static final String toJSONString(Object object, boolean prettyFormat); 
// 将JavaBean转换为JSONObject或JSONArray。
public static final Object toJSON(Object javaObject); 

(3)工具类—JsonUtils

(4)参考链接
FastJSON 简介及其Map/JSON/String 互转

25 Java集合、String互相转换

25.1 数组、List、Set、Map相互转换

Java 集合转换(数组、List、Set、Map相互转换)

25.2 Java数组转成list,list转数组

Java数组转成list,list转数组

25.3 String和String类型的List互相转换(推荐使用24 FastJSON实现Map/Json/String互转)

String和String类型的List互相转换

 /**
     * 集合List转化为字符串
     *
     * @param list
     * @return
     */
    public static String listToString(List list) {
        if (list == null) {
            return "";
        }

        StringBuilder result = new StringBuilder();
        boolean first = true;
        // 第一个前面不拼接","
        for (String string : list) {
            if (first) {
                first = false;
            } else {
                result.append(",");
            }
            result.append(string);
        }
        return result.toString();
    }

    /**
     * 字符串转化为集合List
     *
     * @param strs
     * @return
     */
    public static List stringToList(String strs) {
        String str[] = strs.split(",");
        return Arrays.asList(str);
    }

26 整除(/)、求余(%)、四舍五入

(1)整除(/)、求余(%)

//求余
System.out.println(11%2);     //结果--> 1
System.out.println(11%-2);    //结果--> 1
System.out.println(-11%2);    //结果-->-1
System.out.println(-11%-2);   //结果-->-1
//求余的正负号说明: 主要是取决于前面一个数是正数还是负数,不管后面数。
System.out.println(115%5);     //结果为-->0
System.out.println(115%2);     //结果为-->1

//整除(商)
System.out.println(11/2);     //结果--> 5
System.out.println(11/-2);    //结果-->-5
System.out.println(-11/2);    //结果-->-5
System.out.println(-11/-2);   //结果--> 5
System.out.println(10/2);     //结果--> 5
System.out.println(10.0/2);   //结果--> 5.0
System.out.println(35/2);     //结果--> 17
System.out.println(0.35/2);   //结果--> 0.175

(2)四舍五入

//方式1
DecimalFormat df = new java.text.DecimalFormat("#.00");  
df.format(你要格式化的数字);
//例:#.00 表示两位小数 #.0000四位小数 以此类推...
new java.text.DecimalFormat("#.00").format(3.1415926)

//方式2:%.2f %.表示小数点前任意位数、2表示两位小数、格式后的结果为f表示浮点型 
double d = 3.1415926;
String result = String .format("%.2f");

(3)综合示例

@NonNull
public static String getPlayTimes(int data) {
    String times;
    int MILLION = 10000;
    int BILLION = 100000000;
    DecimalFormat df = new DecimalFormat("#.0");
    if (data < MILLION) {
        times = data + "";
    } else if (data >= MILLION && data < BILLION){
        times = df.format(Double.valueOf(data) / MILLION) + "万";
    } else {
        times = df.format(Double.valueOf(data) / BILLION) + "亿";
    }
    Logger.e("正在观看:" + data + "   " + data + "  " + times);
    // 正在观看:48980   48980.0  4.9万
    return times;
}

(4)参考链接
Android double保留两位小数:截取 和 四舍五入(展示流量)

28 Fragment特点和坑

28.1 Fragment生命周期

Android进阶之开发问题笔记_第37张图片

28.2 Fragment的特点

Fragment的设计主要是把Activity界面包括其逻辑打碎成很多个独立的模块,这样便于模块的重用和更灵活地组装呈现多样的界面。

  • Fragment可以作为Activity界面的一个部分组成;
  • 可以在一个Activity里面出现多个Fragment,并且一个fragment可以在多个Activity中使用;
  • 在Activity运行中,可以动态地添加、删除、替换Fragment。
  • Fragment有自己的生命周期的,可以响应输入事件。

28.3 Fragment嵌套的坑

(1)在Activity中嵌套Fragment要用getSupportFragmentManager
如果用的是v4 support库, getSupportFragmentManager()是activity中的方法, 返回的是管理Activity中Fragments的FragmentManager。(getSupportFragmentManager()主要用于支持 3.0以下android系统API版本,3.0以上系统可以直接调用getFragmentManager())

public class VPFrgActivity extends BaseActivity {
    private List listPagerViews = null;
    private PagerAdapter pagerAdapter = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_vpfrg);
        initViewPager();
    }

    private void initViewPager() {
        listPagerViews = new ArrayList<>();

        listPagerViews.add(new Frg_4());
        listPagerViews.add(new Frg_5());
        listPagerViews.add(new Frg_6());
        ViewPager viewPager = findViewById(R.id.vp_frg);
        pagerAdapter = new FragmentPagerAdapter(this.getSupportFragmentManager()) {
            @Override
            public Fragment getItem(int position) {
                return listPagerViews.get(position);
            }

            @Override
            public int getCount() {
                return listPagerViews.size();
            }

            @Override
            public void setPrimaryItem(ViewGroup container, int position, Object object) {
                super.setPrimaryItem(container, position, object);
            }
        };
        // viewPager.setOffscreenPageLimit(3); // 不设置缓存页面,获取 ViewPath 会变。ViewPgaer 会 destroyItem
        viewPager.setAdapter(pagerAdapter);
    }
}

(2)在Fragment中嵌套Fragment要用getChildFragmentManager
getChildFragmentManager()是Fragment中的方法, 返回的是管理当前Fragment内部子Fragments的manager。

public class Frg_2 extends BaseAndroidXFragment {
    private static FragmentPagerAdapter pagerAdapter = null;
    private View view = null;
    private List listPagerViews = null;

    public Frg_2() { }
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_frg_2, container, false);
        view.findViewById(R.id.tv_frg_2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(v.getContext(), "Frg_2 点击", Toast.LENGTH_SHORT).show();
            }
        });
        initViewPager();
        return view;
    }

    private void initViewPager() {
        listPagerViews = new ArrayList<>();
        listPagerViews.add(new Frg_4());
        listPagerViews.add(new Frg_5());
        listPagerViews.add(new Frg_6());
        ViewPager viewPager2 = (ViewPager) view.findViewById(R.id.frg_2_vp);
        pagerAdapter = new FragmentPagerAdapter(this.getChildFragmentManager()) {
            @Override
            public Fragment getItem(int position) {
                return listPagerViews.get(position);
            }

            @Override
            public int getCount() {
                return listPagerViews.size();
            }

            @Override
            public void setPrimaryItem(ViewGroup container, int position, Object object) {
                super.setPrimaryItem(container, position, object);
            }
        };
        viewPager2.setAdapter(pagerAdapter);
    }
}

(3)学习链接
安卓开发之详解getChildFragmentManager和getFragmentManager详解

Android Fragment使用(二) 嵌套Fragments (Nested Fragments) 的使用及常见错误

getParentFragment() in fragments is returning null when fragment is popped out from backstack

28.4 Fragment使用的坑和正确的使用姿势

Fragment全解析系列(二):正确的使用姿势

28.5 Fragment懒加载

Android仿今日头条多个fragment懒加载的实现

public abstract class LazyloadFragment extends Fragment {

    /**
     * 父容器
     */
    private ViewGroup mViewRoot;
    /**
     * 是否已初始化View
     */
    private boolean mIsCreateView = false;
    /**
     * 是否可见
     */
    private boolean mIsVisible = false;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstancesTate) {
        View rootView = inflater.inflate(R.layout.layout_view_root, container, false);
        mViewRoot = rootView.findViewById(R.id.view_root);

        mIsCreateView = true;
        isCanLoadData();
        return rootView;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        // mIsVisible这个boolean值表示:该fragment的ui,用户是否可见,获取该标志记录下来
        if (isVisibleToUser) {
            mIsVisible = true;
            isCanLoadData();
        } else {
            mIsVisible = false;
        }
    }

    /**
     * view初始化完成并且对用户可见
     */
    private void isCanLoadData() {
        if (mViewRoot != null && mIsCreateView && mIsVisible) {
            initBaseView(mViewRoot);
            lazyInitLoad();

            // 防止重复加载数据
            mIsCreateView = false;
            mIsVisible = false;
        }
    }

    /**
     * 动态加载布局
     *
     * @param viewRoot
     */
    private void initBaseView(ViewGroup viewRoot) {
        viewRoot.removeAllViews();
        View rootView = LayoutInflater.from(getActivity()).inflate(setContentView(), viewRoot, true);
        initView(rootView);
    }

    /**
     * 加载页面布局文件
     *
     * @return
     */
    protected abstract int setContentView();

    /**
     * 让布局中的view与fragment中的变量建立起映射
     * TextView tvInput = rootView.findViewById(R.id.tv_input);
     *
     * @param rootView
     */
    protected abstract void initView(View rootView);

    /**
     * 加载数据
     */
    protected abstract void lazyInitLoad();
}

29 Android 截屏检测

参考云图项目的截图检测+上报逻辑。
RxScreenshotDetector:Android 截屏检测

30 广告栏轮播图

公司多个项目使用:ConvenientBanner

Android图片轮播控件

31 ListView

31.1 ListView的HeaderView/FooterView

(1) ListView的HeaderView/FooterView由于在Adapter里是被ArrayList、SparseArray强引用,不会被回收。适合使用不需要被回收的控件,例如:ViewFlipper。(2018/8/2)

31.2 ListView中Item为EditText获取与保存数据

ListView中Item为EditText获取与保存数据

31.3 ListView的item点击无响应的解决方法

(1)问题描述:如果list item里面包括button或者checkbox等控件,默认情况下listitem会失去焦点,导致无法响应item的事件。
(2)解决办法:在list item的布局文件中设置descendantFocusability属性。
(3)分析:
Android进阶之开发问题笔记_第38张图片
(4)翻译:该属性是当一个为view获取焦点时,定义viewGroup和其子控件两者之间的关系,属性的值有三种:
①beforeDescendants:viewgroup会优先其子类控件而获取到焦点;
②afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点;
③blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点。
(5)学习链接
Android ListView的item点击无响应的解决方法

31.4 ListView.addHeaderView()用法及其注意事项

(1)addHeaderView和setAdapter的顺序问题
①异常提示:java.lang.IllegalStateException: Cannot add header view to list – setAdapter has already been called。
②原因:先调用setAdapter(),再调用addHeaderView(),并且该代码运行在Android4.3之前的系统版本
③解决方法:先调用addHeaderView(),再调用setAdapter()。
④源码分析参考:addHeaderView()异常 —— setAdapter has already been called

(2)ListView用法及其注意事项
Android:ListView.addHeaderView()用法及其注意事项

31.5 ListView的setSelection无效怎么办?

Android 中 ListView 的 setSelection 无效怎么办?

32 RecyclerView

32.1 添加下拉刷新和上拉加载更多

RecyclerView系列之(3):添加下拉刷新和上拉加载更多

一种优雅的方式实现RecyclerView条目多类型

32.2 recycleview获取某个item容易出现空指针问题

public File getItem(int position) {
        if (mFileSortBeans != null && mFileSortBeans.size() > position && position >= 0) {
            FileSortBean fileSortBean = mFileSortBeans.get(position);
            if (fileSortBean != null) {
                return fileSortBean.getFile();
            }
        }
        return null;
  }

32.4 RecyclerView的滚动事件分析

(1)列表的滚动一般分为两种:

手指按下 -> 手指拖拽列表移动 -> 手指停止拖拽 -> 抬起手指
手指按下 -> 手指快速拖拽后抬起手指 -> 列表继续滚动 -> 停止滚动

静止 -> 被迫拖拽移动 -> 静止
静止 -> 被迫拖拽移动 -> 自己滚动 -> 静止

(2)查看源码

public abstract static class OnScrollListener {
        /**
         * 滚动状态变化时回调
         *
         * @param recyclerView 当前滚动的view
         * @param newState     目前的状态,newState有三种值:
         *                     SCROLL_STATE_IDLE = 0:正在滚动
         *                     SCROLL_STATE_DRAGGING = 1:正在被外部拖拽,一般为用户正在用手指滚动
         *                     SCROLL_STATE_SETTLING = 2:自动滚动开
         */
        public void onScrollStateChanged(RecyclerView recyclerView, int newState){}

        /**
         * 滚动时回调
         *
         * @param recyclerView 当前滚动的view
         * @param dx 水平滚动距离,dy>0时为向上滚动
         * @param dy 垂直滚动距离,dy<0时为向下滚动
         */
        public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
    }

(3)判断是否已经到底部或者顶部

/**
 * 垂直方向的判断
 */
public boolean canScrollVertically(int direction) {
	final int offset = computeVerticalScrollOffset();
    final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
    if (range == 0) return false;
    if (direction < 0) {
    	return offset > 0;
    } else {
        return offset < range - 1;
    }
}

// 计算控件垂直方向的偏移值
computeVerticalScrollOffset
// 计算控件可视的区域
computeVerticalScrollExtent
// 计算控件垂直方向的滚动范围
computeVerticalScrollRange

// 水平方向的判断
public boolean canScrollHorizontally(int direction) {
    final int offset = computeHorizontalScrollOffset();
    final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
    if (range == 0) return false;
    if (direction < 0) {
        return offset > 0;
    } else {
        return offset < range - 1;
    }
}
// 设置正数判断是否滑动到底部,返回false表示不能往上滑动,即代表到底部了;
recyclerView.canScrollVertically(1); 
// 设置负数判断是否滑动到顶部, 返回false表示不能往下滑动,即代表到顶部了;
recyclerView.canScrollVertically(-1); 

(4)封装一个可以回调滚动状态和方向的RecyclerView

// 1、先建立事件监听的接口OnScrollCallback
public interface OnScrollCallback {
    void onStateChanged(ScrollRecycler recycler, int state);
    void onScrollUp(ScrollRecycler recycler, int dy);
    void onScrollDown(ScrollRecycler recycler, int dy);
}
// 2、写一个类ScrollRecycler继承RecyclerView,在类中添加以下方法
public void setOnScrollCallback(final OnScrollCallback callback) {
    if (callback == null) {
        return;
    }
    addOnScrollListener(new OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            callback.onStateChanged(ScrollRecycler.this, newState);
        }
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if (dy > 0) {
                callback.onScrollDown(ScrollRecycler.this, dy);
            } else {
                callback.onScrollUp(ScrollRecycler.this, dy);
            }
        }
    });
}

(5)参考链接
RecyclerView的滚动事件分析

34 异常

(1)Java编程的逻辑—125页;(后续总结文章)
(2) Java异常类层次结构图:
Android进阶之开发问题笔记_第39张图片
(3)判断一个方法可能会出现异常的依据如下:

// ①方法中有throw语句;
// ②调用了其他方法,其他方法的括号后面用throws子句声明抛出某种异常。

(4)学习链接:JAVA 异常分类与理解

37 打印当前的线程及进程

(1)基本方法

// 获取消耗的时间。
android.os.Process.getElapsedCpuTime() 
// 获取该进程的ID
android.os.Process.myPid()
// 获取该线程的ID
android.os.Process.myTid()
// 获取该进程的用户ID
android.os.Process.myUid()
// 判断该进程是否支持多进程
android.os.Process.supportsProcesses

(2)参考链接
android如何打印当前的线程及进程

39 Activity之间跳转(或者关闭多个Activity)出现短暂黑屏的处理方法

39.1 自定义Theme(主题)

/**
 * 设置目标 Activity(要启动的那个Activity)是透明的。
 * 两个Activity,A和B;在A中启动B,因为B是透明的,看到的背景仍是 A,这样就解决了这个短暂的黑屏问题。
 */
 

39.2 修改Manifest,将Theme应用到目标Activity上


40 收起输入法时会出现屏幕部分黑屏解决

(1)问题描述:在Android开发过程中,如果使用EditText弹出了输入法时,可能会遇到如下情况:在收起输入法时,手机屏幕被输入法挡住的部分会变为黑色,部分黑屏。

(2)思考与想法:寻找解决办法很久,但没找到。想到这不应该是自定义布局背景的原因,那么显示黑屏的部分就应该是从更深层次的view的背景引起的,那么使用自定义的view通过函数getRootView()函数来获取其上一层view并修改其背景色即可。
(3)举例

View llRoot = findViewById(R.id.ll_root);
llRoot.getRootView().setBackgroundResource(R.color.white);

(4)参考链接
关于Android收起输入法时会出现屏幕部分黑屏解决

41 Lottie使用

41.1 解决第一次加载空白问题

private void setAnimationData() {
    if (mFromFileSync) {
        // 同步加载(修复进入App第一次加载延迟问题)
        LottieComposition composition = LottieComposition.Factory.fromFileSync(mParent, Constants.ANIMATION_LOADING);
        mAnimationView.setComposition(composition);

        // 同时异步缓存(缓存STRONG_REF_CACHE使用static)
        mAnimationView.setAnimation(Constants.ANIMATION_LOADING, LottieAnimationView.CacheStrategy.Strong);
    } else {
        // 异步加载(可以使用缓存)
        mAnimationView.setAnimation(Constants.ANIMATION_LOADING, LottieAnimationView.CacheStrategy.Strong);
    }

    mAnimationView.loop(true);
}

41.2 参考链接

官方—airbnb/lottie-android

QQ音乐技术团队—Lottie : 让动画如此简单

博客专家—Lottie的使用及原理浅析

Android Lottie动画的简单使用

lottie加载动画,第一次有延迟问题:第一次使用同步加载、同时设置异步加载缓存数据

42 启动页冷启动适配全面屏和虚拟键

42.1 解决




    
    


    /**
     * 隐藏虚拟按键
     * 在SplashActivity的onCreate()添加此方法
     */
    private void hideBottomUIMenu() {
        // 隐藏虚拟按键
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            View v = this.getWindow().getDecorView();
            v.setSystemUiVisibility(View.GONE);
        } else if (Build.VERSION.SDK_INT >= 19) {
            View decorView = getWindow().getDecorView();
            int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
            decorView.setSystemUiVisibility(uiOptions);
        }
    }

42.2 参考链接

android 启动页冷启动适配全面屏和虚拟键

43 Activity的变量,在断点调试下:No such instance field

(1)问题如图
Android进阶之开发问题笔记_第40张图片
(2)解决方案:检查build.gradle文件中buildtypes/debug 是否打开了混淆,关闭混淆。
(3)学习链接:Activity中所有的变量,在断点调试下都是No such instance field

46 调试手机中数据库的工具:Android-Debug-Database

(1)目前的工具的弊端

  • 将手机中的SQLite数据库导出到电脑,通过电脑端的软件来查看这个数据库,执行相关的SQL语句,看结果如何。
  • Root手机,在手机上安装RE文件管理器,进入应用程序的包下,找到你的数据库的文件,然后再查看数据库中。
  • Android Studio有相关的插件,方便操作,但是有的需要收费,使用起来也不是很爽。

(2)Android-Debug-Database的特色

  • 可以在浏览器查看你的应用中所有的数据库。
  • 可以查看你的应用中所有的shared preferences(额外福利)。
  • 在浏览器对你指定的数据库执行SQL语句,对你指定的数据库中的数据进行可视化的编辑,以及将数据库直接下载下来。

(3)如何使用

  • 在你的build.gradle添加如下:
// debugCompile的作用:只在你debug编译时起作用,当你release的时候就不要使用
debugCompile 'com.amitshekhar.android:debug-db:1.0.0'
  • 在App启动的时候,你要注意查看下你的logcat,会有这么一行,把它复制到你电脑的浏览器,你就可以看到你的App中的数据库,和shared preferences。
D/DebugDB: Open http://XXX.XXX.X.XXX:8080
  • 界面如下
    Android进阶之开发问题笔记_第41张图片

(4)原理-参考链接
调试手机中数据库的福音:Android-Debug-Database

Github-Android-Debug-Database

47 SharedPreferences中的commit和apply方法

(1)SharedPreferences对象获取

// Context类中的getSharedPreferences方法可以获取一个SharedPreferences对象
SharedPreferences mSp = mContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
// 其本质也是在文件不存在的情况下new一个File
private File makeFilename(File base, String name) {
	if (name.indexOf(File.separatorChar) < 0) {
		return new File(base, name);
	}
	throw new IllegalArgumentException( "File " + name + " contains a path separator");
}

(2)commit方法

 /**
  * commit方法是有一个boolean的返回值
  * 当数据变化进行存储时是一个原子性的操作
  * 当两个editor对象同时对一个共享的preferences参数进行操作时,永远都是最后一个调用commit方法的editor变更了最后的数据值
  */
boolean commit();

(3)apply方法

/**
 * apply方法是没有返回值的
 * 当两个editor同时对preferences对象编辑时,也是最后一个调用apply方法的对象编辑数据
 * apply的提交操作也是原子性的
 */
void apply();

(4)commit和apply方法的区别

  • commit和apply虽然都是原子性操作,但是原子的操作不同,
  • apply提交时异步过程。操作是原子提交的内存中,而非数据库,在提交到内存中时不可打断,之后再异步提交数据到数据库中,因此也不会有相应的返回值
  • commit提交是同步过程。操作是原子提交到数据库,从提交数据到存入Disk中都是同步过程,中间不可打断,效率会比apply异步提交的速度慢,但有返回值,可知道存储是否失败
  • 在不关心提交结果是否成功的情况下,优先考虑apply方法。

(5)学习链接
SharedPreferences中的commit和apply方法

49 Textview难点解析

49.1 使用代码为Textview设置drawableLeft

(1)在XML中使用

android:drawableLeft="@drawable/icon"

(2)代码中动态设置

Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_mark_big_new);
if (drawable != null) {
	// 这一步必须要做,否则不会显示.
	drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight());
	textview.setCompoundDrawables(null, null, drawable, null);

	// 设置字体颜色
	textView.setTextColor(getResources().getColor(R.color.gray_textcolor_shen));
	// 设置图片和text之间的间距 
	textView.setCompoundDrawablePadding(DeviceUtils.dp2px(context, 4));
	// 设置整个textView的间距
	textView.setPadding(-5, 0, 0, 0);
	// 设置整个textView透明度(包括:图片和text)
	textView.setAlpha(0.64f);
}

(3)也或参考另一个函数

public void setCompoundDrawablesWithIntrinsicBounds (Drawable left,
Drawable top, Drawable right, Drawable bottom)

(4)学习链接
使用代码为textview设置drawableLeft

49.2 TextView设置字体粗体

(1)最简单的布局方法

android:textStyle="bold"

(2)代码中设置字体粗体

TextView textView = (TextView)findViewById(R.id.textView);
// android中为textview动态设置字体为粗体
textView .setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
// 设置不为加粗
textView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));

49.3 布局中的空格以及占一个汉字宽度的空格的实现

(1)效果图
这里写图片描述
(2)代码实现



    
    
    
    
    
    
    
    
    

public class Main2Activity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);

        TextView text4 = (TextView) findViewById(R.id.text4);
        text4.setText("8、姓" + "\u3000" + "名");
    }
}

(3)总结:
①7和8都是一个汉字的间隔。其中,在代码中设置text4.setText(“8、姓” + “\u3000” + “名”);的"\u3000"是一个汉字的间隔;
②4是稍大的空格;
③5是稍小的空格。
(4)参考链接
Android布局中的空格以及占一个汉字宽度的空格的实现

49.4 ColorStateList使按钮文字变色

// XML文件保存在res/color/button_text.xml


    
    
    

// 1.代码中
Button btn = (Button)findViewById(R.id.btn); 
Resources resource = (Resources)getBaseContext().getResources();  
ColorStateList csl = (ColorStateList)resource.getColorStateList(R.color.button_text); 
if(csl != null){  
	// 设置按钮文字颜色 
	btn.setTextColor(csl);
}  
// 2.布局中

49.5 填lineSpacingExtra的坑,解决行间距兼容性问题

填填Android lineSpacingExtra 的坑,解决行间距兼容性问题

49.6 设置TextView中部分字体颜色和点击事件

(1)将字符串中的多个关键字高亮&字体大小&是否加粗

public static SpannableStringBuilder highlightMultiKeywordSize(String original, Map keywordMap, int color, int size, boolean isBold) {
        SpannableStringBuilder builder = new SpannableStringBuilder(original);
        if (keywordMap == null) {
            return builder;
        }

        for(Map.Entry entry : keywordMap.entrySet()){
            if (!TextUtils.isEmpty(entry.getKey())) {
                int start = original.indexOf(entry.getKey());
                int end = start + entry.getKey().length();
                if (start >= 0 && start <= end && start < original.length() && end <= original.length()) {
                    ForegroundColorSpan span = new ForegroundColorSpan(color);
                    builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

                    AbsoluteSizeSpan sizeSpan = new AbsoluteSizeSpan(size);
                    builder.setSpan(sizeSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

                    if (isBold) {
                        StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
                        builder.setSpan(styleSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }

                    if (entry.getValue() != null) {
                        builder.setSpan(entry.getValue(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                }
            }
        }
        return builder;
    }
/**
     * 获取高亮的字符串-点击事件HashMap
     * @param context
     * @return
     */
    @NonNull
    private Map getStringClickMap(Context context) {
        String licenseAgreement = context.getString(R.string.qihoo_accounts_license_agreement_prompt_license_agreement);
        String privacyProtection = context.getString(R.string.qihoo_accounts_license_agreement_prompt_privacy_protection);

        Map keywordMap = new HashMap<>(4);
        keywordMap.put(context.getString(R.string.privacy_dialog_quotes, licenseAgreement), new ClickableSpan() {
            @Override
            public void onClick(@NonNull View widget) {
                ReaderWebActivity.actionStart(context, licenseAgreement, UrlConfigManager.getInstance().getUrl(UrlConfigManager.LICENSE_AGREEMENT_URL));
            }
            @Override
            public void updateDrawState(@NonNull TextPaint ds) {
                // 设置下划线 true显示、false不显示
                ds.setUnderlineText(false);
            }
        });
        return keywordMap;
    }

(2)应用

/**
     * 获取消息文案
     * @param context
     * @return
     */
    private SpannableStringBuilder getMessageStr(Context context) {
        Map spanMap = getStringClickMap(context);
        int color = ContextCompat.getColor(context, R.color.orange);
        int size = DeviceUtils.dp2px(context, 14);

        String message = context.getString(R.string.privacy_dialog_message);
        SpannableStringBuilder spanStrBuilder = StringUtils.highlightMultiKeywordSize(message, spanMap, color, size, true);
        return spanStrBuilder;
    }
public void setMessage(CharSequence msg) {
        mTvMessage.setText(msg);
        // 设置点击后的颜色为透明
        mTvMessage.setHighlightColor(ContextCompat.getColor(mContext, R.color.transparent));
    }

(3)学习链接
Android设置TextView中部分字体颜色和点击事件

50 如何理解Android中的xmlns(android/app/tools)

(1)命名空间(namespace)
xmlns是XML文档中的一个概念:英文叫做XML namespace,中文翻译为XML命名空间。XML命名空间提供避免元素命名冲突的方法。打个比方:A学校有名学生叫做林小明,B学校也有名学生叫林小明,那我们如何识别这两名拥有相同名字的同学呢?这时候命名空间就派上用场了,A和B此时就可以被当成是命名空间了。也就是说,命名空间里面存放的是特定属性的集合。
(2)Android中常见的命名空间:android

// android:称作namespace-prefix,它是命名空间的名字 
// http://schemas.android.com/apk/res/android看起来是一个URL,但是这个地址是不可访问的。实际上这是一个URI(统一资源标识符),所以它的值是固定不变的,相当于一个常量。
xmlns:android=”http://schemas.android.com/apk/res/android” 
// 例子


(3)tools:只作用于开发阶段,当app被运行/打包时所有关于tools属性将都会被摒弃掉

xmlns:tools=”http://schemas.android.com/tools”
// 例子


    

视图窗口(Design)中查看时,看到的是标签顶部居中显示:
Android进阶之开发问题笔记_第42张图片
当我们运行到手机上时,确是这样的:
Android进阶之开发问题笔记_第43张图片
(4)自定义命名空间:app

xmlns:app="http://schemas.android.com/apk/res-auto"

(5)学习链接
Android中XML的命名空间、自定义属性

51 在同一个xml文件中同时引用多个同一个layout文件时,使用间接findViewById的方式获取include内部的组件

(1)my_include.xml

  
  
      
  

(2)activity_main.xml

  
  
      
  
      
  

(3)MainActivity.java

protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
          
        //第一种情况:间接得到文本组件(会得到两个)  
        textView1=(TextView) findViewById(R.id.include1).findViewById(R.id.text);  
        textView2=(TextView) findViewById(R.id.include2).findViewById(R.id.text);  
        //第二种情况:直接得到文本组件  
        textView3=(TextView) findViewById(R.id.text);  
        //更改textView3的文字  
        textView3.setText("啦啦啦啦啦啦啦啦啦");  
    }  

(4)结论:通过间接和直接的方式都可以获取include内部的组件。

(5)因此:在同一个xml文件中同时引用多个同一个layout文件时,笔者推荐大家使用间接findViewById的方式获取include内部的组件。

(6)参考链接
过安卓中标签findViewById时出现的bug及解决方案

52 dp、px的关系与换算

(1)关系图示–备注:1:3的比例是按密度区分,不是按尺寸
Android进阶之开发问题笔记_第44张图片
(2)学习链接
android适配(一) 之dp、dip、dpi、px、sp简介及相关换算
一套效果图适配(Android和IOS)全尺寸和标注规范
移动应用(APP)UI设计规范之尺寸篇上

53 快速切换到主线程更新UI的5种方法–前提:都运行在子线程中

(1)view.post(Runnable action)

// 这是view自带的方法,如果你的子线程里可以得到要更新的view的话,可以用此方法进行更新。
// view还有一个方法view.postDelayed(Runnable action, long delayMillis)用来延迟发送。
textView.post(new Runnable() {
        @Override
        public void run() {
            textView.setText("更新textView");
            // 还可以更新其他的控件
            imageView.setBackgroundResource(R.drawable.update);
        }
    });

(2)activity.runOnUiThread(Runnable action)

/**
 * 如果没有上下文(context),可以用view.getContext()可以得到上下文;
 * 跳过context直接用new Activity().runOnUiThread(Runnable action)来切换到主线程。
 */
public void updateUI(final Context context) {
        ((MainActivity) context).runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // 此时已在主线程中,可以更新UI了
            }
        });
    }

(3)Handler机制

// 在子线程中
android.os.Handler mainHandler = new android.os.Handler(Looper.getMainLooper());
mainHandler.post(new Runnable() {
        @Override
        public void run() {
            // 已在主线程中,可以更新UI
        }
    });
    
new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        
                    }
                });
// 假设在主线程中
Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case 0:
                    //更新UI等
                    break;
                default:
                    break;
            }
        }
    }

(4)使用AsyncTask,执行顺序依次为:onPreExecute, doInBackground, onPostExecute

    private class MyAsyncTask extends AsyncTask {
        protected String doInBackground(String... params) { // 子线程中执行,执行一些耗时操作,关键方法,在执行过程中可以调用publishProgress(Progress... values)来更新进度信息。
            System.out.println("MyAsyncTask.doInBackground");
            // 只是模拟了耗时操作
            int count = 0;
            for (int i = 0; i < 10; i++) {
                try {
                    count++;
                    publishProgress((count % 100) * 10);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return "耗时操作执行完毕";
        }
        @Override
        protected void onProgressUpdate(Integer... values) { // 主线程中执行:在调用publishProgress(Progress... values)时,此方法被执行,直接将进度信息更新到UI组件中
            super.onProgressUpdate(values);
            progressBar.setProgress(values[0]);
            textView.setText("loading..." + values[0] + "%");
        }
        @Override
        protected void onPostExecute(String aVoid) { // 在主线程执行:当后台操作结束时,此方法将会被调用,计算结果将做为参数传递到此方法中
            super.onPostExecute(aVoid);
            textView.setText(aVoid);
        }
}

(5)RxJava的主线程和子线程切换

	/**
     * 数据库获取我的群组列表未读消息
     *
     * @param list
     * @param refreshInfoListener
     */
    @Override
    public void getMyGroupListUnRead(List list, IRefreshInfoListener refreshInfoListener) {
        mRefreshUnReadObservable = Observable.create(new ObservableOnSubscribe>() {
            @Override
            public void subscribe(final ObservableEmitter> emitter) throws Exception {
                List groupList = new ArrayList<>(list);
                List list = IMSessionDAO.queryAllSessionList(IMSessionDAO.QUERY_TYPE.GROUP);
                for (GroupInfo groupInfo : groupList) {
                    for (IMSessionBean imSession : list) {
                        if (TextUtils.equals(groupInfo.groupId, imSession.getSessionId())) {
                            groupInfo.unreadCount = imSession.getUnreadCount();
                            break;
                        }
                    }
                }
                emitter.onNext(groupList);
            }
        }).compose(InjectionUtils.schedulersTransformer())
                .subscribe(new Consumer>() {
                    @Override
                    public void accept(List groupList) throws Exception {
                        if (groupList != null && groupList.size() > 0) {
                            refreshInfoListener.onSuccess(groupList);
                        }
                    }
                });
    }

(6)学习链接
快速切换到主线程更新UI的几种方法

54 在AS中导入jniLibs目录

(1)编辑app目录下的build.gradle文件,为其添加以下代码

android {
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
    // 或者: 
    sourceSets.main {
        jniLibs.srcDirs = ['libs']
        jni.srcDirs = []
    }
}

55 组合逗号String&根据逗号分隔String

(1)组合逗号String

ArrayList collectIds = new ArrayList<>();
String str = collectIds.toString();
int len = str.length() - 1;
String ids = str.substring(1, len).replace(" ", "");//"keyids":”1,2,3”

(2)根据逗号分隔String

String str="112,123,123,123";//根据逗号分隔到List数组中
String str2=str.replace(" ", "")//去掉所用空格
List list= Arrays.asList(str2.split(","))
//list的结果就是[113,123,123,123]

56 implementation、api和compile区别

(1)api指令
api指令完全等同于compile指令。
(2)implementation指令
这个指令的特点就是,对于使用了该命令编译的依赖,对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序,也就是将该依赖隐藏在内部,而不对外部公开。
(3)主要区别
implementation不可以依赖传递,但是compile可以依赖传递。 谁长(单词多堪称人多)谁(排队)检查,(为了)安全才复杂。
(3)文不如图
Android进阶之开发问题笔记_第45张图片
备注:app implementation module1 ,module1 compile module2,app依然可以访问到module2的api。

(4)建议
在Google IO 相关话题的中提到了一个建议,就是依赖首先应该设置为implementation,如果没有错,那就用implementation;如果有错,那么使用api指令,这样会使编译速度有所增快。
(5)学习链接
implementation 和 compile区别

android gradle tools 3.X 中依赖,implementation 和compile区别

57 不同Module中正确依赖aar

57.1 在app module中依赖aar文件

(1)将aar文件拷入app module下的libs文件夹下;
(2)在app的build.gradle文件中配置如下:

android {
}
repositories {
    flatDir {
        dirs 'libs'
    }
}
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    // 在dependencies添加依赖并同步即可
    implementation (name:'torch-adcore-5.4.3105',ext:'aar')
}

(3)注:repositories和android、dependencies处在同一级别的目录下。如不添加该配置,同步依赖会失败,提示 Failed to resolve。

57.2 在library module中依赖aar文件

(1)将aar文件拷入想要依赖的library module下的libs文件夹中;
(2)在module或project中声明aar文件路径:
以此例子: app implementation module1 ,module1 compile module2

// 方式1:在module1的build.gradle文件中配置如下:
repositories {
    flatDir {
        dirs 'libs'
    }
}

// 在app的build.gradle文件中配置如下:
repositories {
    flatDir {
        dirs '../xxx/libs','libs'  //将xxx替换为引入aar文件的module名
    }
}
// 方式2:在app的build.gradle文件中统一配置如下:
allprojects {
    repositories 
        flatDir {
            dirs project(':xxx').file('libs')  // 将xxx替换为引入aar文件的module名
        }
    }
}

(3)在依赖aar文件的library module下的build.gradle文件中添加依赖如下:

// 在module1中
api (name:'xxx',ext:'aar')  
 // 在app中
implementation(name: 'xxx', ext: 'aar') 

(4)批量导入libs里的所有.aar类库

fileTree(dir: 'libs', include: '**/*.aar')
        .each { File file ->
    dependencies.add("api", [
            name: file.name.lastIndexOf('.').with { it != -1 ? file.name[0..

(5)举例子

// 在adsdklib的build.gradle文件中
// 文件的指向
repositories{
    flatDir{
        dirs 'libs'
    }
}

// 批量导入libs里的所有.aar类库
fileTree(dir: 'libs', include: '**/*.aar')
        .each { File file ->
    dependencies.add("api", [
            name: file.name.lastIndexOf('.').with { it != -1 ? file.name[0..
// 在app的build.gradle文件中
repositories{
    flatDir{
        dirs '../adsdklib/libs'
        // dirs project(':adsdklib').file('libs')  // 等价于
    }
}

(6)注:所有直接或间接依赖该library module的module中都应声明该aar文件所在libs目录的相对路径或在project的build.gradle文件中进行统一配置声明。否则会同步依赖失败,提示 Failed to resolve。
(7)学习链接
在Android Studio的不同Module中依赖aar的配置问题
android studio library 模块中正确引用aar

58 intent.putExtra()和bundle.putExtra()的区别

(2)在Activity间传数据,我们一般有两种方式:

// 1、直接用Intent的putExtra(), getStringExtra();
// 2、先new一个Bundle对象,用Bundle的putExtra().

(2)先看一下Intent对象相应函数的源代码:

public Intent putExtra(String name, String value) {
        if (mExtras == null) {
            mExtras = new Bundle();
        }
        mExtras.putString(name, value);
        return this;
}
public String getStringExtra(String name) {
        return mExtras == null ? null : mExtras.getString(name);
}

(3)Intent的相关操作的实现是基于Bundle的,Bundle操作数据相对于Intent有哪些优势呢?

如果要从ActivityA跳转到ActivityB和ActivityC,那么我要写两个Intent,如果传的数据相同,我两个Intent都要添加,
但是如果我用Bundle,直接一次性包装就可以了。

再有,如果ActivityA要传值给ActivityB,再从ActivityB传值到ActivityC,用Intent是多么的麻烦了,
但是用Bundle就很简洁,而且还可以在Activityb添加新的信息。

59 SwipeRefreshLayout嵌套Webview滑动冲突问题解决

(1)问题描述:有一个需求,需要在网页中实现下拉刷新功能,由于两个控件都有滑动的事件,在向下滑动的时候滑动事件被SwipeRefreshLayout控件消费了,但是Webview本身是不滑动的,而是H5在滑动。可以通过桥获取H5传递的滑动距离,对SwipeRefreshLayout是否允许下拉刷新进行控制。
(2)具体内容



        
    
public class MyWebView extends WebView {
    public MyWebView(Context context) {
        super(context);
    }
    public MyWebView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
    }
    public MyWebView(Context context, AttributeSet attributeSet, int i) {
        super(context, attributeSet, i);
    }

    /**
     * 设置WebView滚动高度ScrollY
     */
    public void setWebViewScrollY(int scrollY) {
        if (mScrollListener != null) {
            mScrollListener.onScrollY(scrollY);
        }
    }

    public interface IScrollListener {
        void onScrollY(int scrollY);;
    }

    private IScrollListener mScrollListener;

    public void setOnScrollListener(IScrollListener listener) {
        mScrollListener = listener;
    }
}
mWebView.setOnScrollChangedListener(new NovelWebView.OnScrollChangedListener() {
                @Override
                public void onScrollY(int scrollY) {
                    if (scrollY == 0) {
                        mSwipeRefreshLayout.setEnabled(true);
                    } else {
                        mSwipeRefreshLayout.setEnabled(false);
                    }
                }
            });
public class WebViewInterface {
 /**
     * h5页面传递客户端滚动高度
     */
    public static void setScrollY(WebView view, String scrollY) {
        if (view == null) {
            return;
        }
        ((NovelWebView) view).setWebViewScrollY(getIntForString(scrollY));
    }

/**
     * 从字符串中获取数字
     * @param strInput
     * @return
     */
    public static int getIntForString(String strInput) {
        try {
            // 匹配指定范围内的数字
            String regEx = "[^0-9]";
            // Pattern是一个正则表达式经编译后的表现模式
            Pattern p = Pattern.compile(regEx);
            // 一个Matcher对象是一个状态机器,它依据Pattern对象做为匹配模式对字符串展开匹配检查。
            Matcher m = p.matcher(strInput);
            // 将输入的字符串中非数字部分用空格取代并存入一个字符串
            String string = m.replaceAll(" ").trim();
            // 以空格为分割符在讲数字存入一个字符串数组中
            String[] strArr = string.split(" ");
            if (strArr.length > 0) {
                return Integer.valueOf(strArr[0]);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }
}

(3)参考链接(但是本方法存在问题)
Android SwipeRefreshLayout嵌套Webview滑动冲突问题解决

60 Snackbar

(1)Snackbar是Android Support Design Library库中的一个控件,可以在屏幕底部快速弹出消息,比Toast更加好用。本文对原生Snackbar进行了修改,使其更加灵活。

(2)Android Snackbar花式使用指南

61 shrinkResources

61.1 shrinkResources true引发的问题及解决

(1)shrinkResources true作用
在应用构建打包的过程中自动删除没有引用的资源,对依赖的库同样有效shrinkResources必须和minifyEnabled配合使用达到减少包体积的作用,只有删除了无用的代码之后,才能知道哪些资源是无用的。

(2)保持资源
和混淆可以配置keep类似,资源同样也可以通过tools:keep来指定要保留的资源文件,通常通过res/raw/keep.xml指定要保持的资源,



同样可以通过 tools:discard 来从保持的资源中指定要删除的资源,如

tools:discard="@layout/unused2" 

(3)解决问题方法:为了指定SDK中的图片资源不被删除,可以配置res/raw/keep.xml文件来指定要保持的资源文件

(4)学习链接
官方文档-resource-shrinking

shrinkResources true引发的问题及解决

62 关于xml中clipChildren属性的用法

(1)类似下面的一个底部tab选项卡布局。中间的是凸出来一点。类似这样:
在这里插入图片描述
(2)实现方法




    

    

        

            // 用layout_gravity来控制超出的部分显示位置

            

            
        
    

  // 在根布局节点设置clipChildren=false,默认为true:是否限制子视图在其范围内

(3)注意
在merge标签中设置android:clipChildren="false"是无效的,必须在有效根布局中设置。
(4)学习链接
关于xml中clipChildren属性的用法

63 背景与前景问题

63.1 src和background的区别

(1)src是图片内容(前景);background是背景;
(2)src就存放的是原图的大小,不会进行拉伸;background会根据ImageView组件给定的长宽进行拉伸;
(3)scaleType只对src起作用;background可设置透明度;
(4)通过代码和背景设置图片

// src
android:src="@drawable/logo" 
iv.setImageResource(R.drawable.);
//  background
android:background="@drawable/logo" 
iv.setBackgroundResource(R.drawable.);

(5)getDrawable()与getBackground()

protected void onDraw(Canvas canvas) {
	// src:getDrawable()
	// background: getBackground()
}

(5)学习链接
Android src和background的区别

63.2 前景圆角的FrameLayout,可设置宽高比例

(1)attr_widget.xml


    
        
        
        
        
        
        
        
        
        
    

(2)RoundAngleFrameLayout.java

public class RoundAngleFrameLayout extends FrameLayout {
    private float mTopLeftRadius = 0;
    private float mTopRightRadius = 0;
    private float mBottomLeftRadius = 0;
    private float mBottomRightRadius = 0;
    private int mRatioWidth = 0;
    private int mRatioHeight = 0;

    private Paint mRoundPaint;
    private Paint mImagePaint;

    public RoundAngleFrameLayout(Context context) {
        this(context, null);
    }

    public RoundAngleFrameLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RoundAngleFrameLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        if (attrs != null) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundAngleFrameLayout);
            float radius = ta.getDimension(R.styleable.RoundAngleFrameLayout_round_radius, 0);
            mTopLeftRadius = ta.getDimension(R.styleable.RoundAngleFrameLayout_topLeftRadius, radius);
            mTopRightRadius = ta.getDimension(R.styleable.RoundAngleFrameLayout_topRightRadius, radius);
            mBottomLeftRadius = ta.getDimension(R.styleable.RoundAngleFrameLayout_bottomLeftRadius, radius);
            mBottomRightRadius = ta.getDimension(R.styleable.RoundAngleFrameLayout_bottomRightRadius, radius);
            mRatioWidth = ta.getInt(R.styleable.RoundAngleFrameLayout_ratioWidth, 0);
            mRatioHeight = ta.getInt(R.styleable.RoundAngleFrameLayout_ratioHeight, 0);
            ta.recycle();
        }

        mRoundPaint = new Paint();
        mRoundPaint.setColor(Color.WHITE);
        mRoundPaint.setAntiAlias(true);
        mRoundPaint.setStyle(Paint.Style.FILL);
        mRoundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));

        mImagePaint = new Paint();
        mImagePaint.setXfermode(null);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mRatioWidth > 0 && mRatioHeight > 0) {
            int originalWidth = View.MeasureSpec.getSize(widthMeasureSpec);
            int calculatedHeight = originalWidth * mRatioHeight / mRatioWidth;
            super.onMeasure(View.MeasureSpec.makeMeasureSpec(originalWidth, View.MeasureSpec.EXACTLY),
                    View.MeasureSpec.makeMeasureSpec(calculatedHeight, View.MeasureSpec.EXACTLY));
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.saveLayer(new RectF(0, 0, canvas.getWidth(), canvas.getHeight()), mImagePaint, Canvas.ALL_SAVE_FLAG);
        super.dispatchDraw(canvas);
        drawTopLeft(canvas);
        drawTopRight(canvas);
        drawBottomLeft(canvas);
        drawBottomRight(canvas);
        canvas.restore();
    }

    private void drawTopLeft(Canvas canvas) {
        if (mTopLeftRadius > 0) {
            Path path = new Path();
            path.moveTo(0, mTopLeftRadius);
            path.lineTo(0, 0);
            path.lineTo(mTopLeftRadius, 0);
            path.arcTo(new RectF(0, 0, mTopLeftRadius * 2, mTopLeftRadius * 2), -90, -90);
            path.close();
            canvas.drawPath(path, mRoundPaint);
        }
    }

    private void drawTopRight(Canvas canvas) {
        if (mTopRightRadius > 0) {
            int width = getWidth();
            Path path = new Path();
            path.moveTo(width - mTopRightRadius, 0);
            path.lineTo(width, 0);
            path.lineTo(width, mTopRightRadius);
            path.arcTo(new RectF(width - 2 * mTopRightRadius, 0, width, mTopRightRadius * 2), 0, -90);
            path.close();
            canvas.drawPath(path, mRoundPaint);
        }
    }

    private void drawBottomLeft(Canvas canvas) {
        if (mBottomLeftRadius > 0) {
            int height = getHeight();
            Path path = new Path();
            path.moveTo(0, height - mBottomLeftRadius);
            path.lineTo(0, height);
            path.lineTo(mBottomLeftRadius, height);
            path.arcTo(new RectF(0, height - 2 * mBottomLeftRadius, mBottomLeftRadius * 2, height), 90, 90);
            path.close();
            canvas.drawPath(path, mRoundPaint);
        }
    }

    private void drawBottomRight(Canvas canvas) {
        if (mBottomRightRadius > 0) {
            int height = getHeight();
            int width = getWidth();
            Path path = new Path();
            path.moveTo(width - mBottomRightRadius, height);
            path.lineTo(width, height);
            path.lineTo(width, height - mBottomRightRadius);
            path.arcTo(new RectF(width - 2 * mBottomRightRadius, height - 2 * mBottomRightRadius, width, height), 0, 90);
            path.close();
            canvas.drawPath(path, mRoundPaint);
        }
    }
}

(3)布局使用



        
    

(4)效果
Android进阶之开发问题笔记_第46张图片
(5)RoundAngleFrameLayout设置的前景,所以不可以直接设置背景,所以以下不生效


63.3 带边框的圆角、圆形ImageView(背景+前景)

(1)控件参考:Android 圆角、圆形 ImageView 实现
(2)项目中的控件:RoundRectImageView

63.4 通过Canvas对Bitmap进行圆角处理(前景)

(1)工具类

public class RoundBitmapFillet {
    // 圆角类型
    @IntDef({TYPE.ALL, TYPE.TOP, TYPE.LEFT, TYPE.RIGHT, TYPE.BOTTOM})
    @Retention(RetentionPolicy.SOURCE)
    public @interface TYPE {
        int ALL = 0;
        int TOP = 1;
        int LEFT = 2;
        int RIGHT = 3;
        int BOTTOM = 4;
    }

    /**
     * 指定图片的切边,对图片进行圆角处理
     *
     * @param bitmap
     * @param type
     * @param radius
     * @param width
     * @param height
     * @return
     */
    public static Bitmap filletRoundBitmap(Bitmap bitmap, @TYPE int type, int radius, int width, int height) {
        try {
            // (1)先建立一个与图片大小相同的透明的Bitmap画板
            Bitmap paintingBoard = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(paintingBoard);

            // (2)在画板上画出一个想要的形状的区域
            final Paint paint = new Paint();
            if (TYPE.TOP == type) {
                clipTop(canvas, paint, radius, width, height);
            } else if (TYPE.LEFT == type) {
                clipLeft(canvas, paint, radius, width, height);
            } else if (TYPE.RIGHT == type) {
                clipRight(canvas, paint, radius, width, height);
            } else if (TYPE.BOTTOM == type) {
                clipBottom(canvas, paint, radius, width, height);
            } else {
                clipAll(canvas, paint, radius, width, height);
            }

            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
            // (3)把源图片帖上
            Rect src = new Rect(0, 0, width, height);
            canvas.drawBitmap(bitmap, src, src, paint);
            return paintingBoard;
        } catch (Exception exp) {
            return bitmap;
        }
    }

    private static void clipLeft(final Canvas canvas, final Paint paint, int offset, int width, int height) {
        final Rect block = new Rect(offset, 0, width, height);
        canvas.drawRect(block, paint);
        final RectF rectF = new RectF(0, 0, offset * 2, height);
        canvas.drawRoundRect(rectF, offset, offset, paint);
    }

    private static void clipRight(final Canvas canvas, final Paint paint, int offset, int width, int height) {
        final Rect block = new Rect(0, 0, width - offset, height);
        canvas.drawRect(block, paint);
        final RectF rectF = new RectF(width - offset * 2, 0, width, height);
        canvas.drawRoundRect(rectF, offset, offset, paint);
    }

    private static void clipTop(final Canvas canvas, final Paint paint, int offset, int width, int height) {
        final Rect block = new Rect(0, offset, width, height);
        canvas.drawRect(block, paint);
        final RectF rectF = new RectF(0, 0, width, offset * 2);
        canvas.drawRoundRect(rectF, offset, offset, paint);
    }

    private static void clipBottom(final Canvas canvas, final Paint paint, int offset, int width, int height) {
        final Rect block = new Rect(0, 0, width, height - offset);
        canvas.drawRect(block, paint);
        final RectF rectF = new RectF(0, height - offset * 2, width, height);
        canvas.drawRoundRect(rectF, offset, offset, paint);
    }

    private static void clipAll(final Canvas canvas, final Paint paint, int offset, int width, int height) {
        final RectF rectF = new RectF(0, 0, width, height);
        canvas.drawRoundRect(rectF, offset, offset, paint);
    }
}

(2)使用方法

Bitmap bmOutput = RoundBitmapFillet.filletRoundBitmap(bmInput, RoundBitmapFillet.TYPE.ALL, mRadius, mDesWidth, mDesHeight);

(3)效果
Android进阶之开发问题笔记_第47张图片
(4)学习链接
【Android】图片切角,切指定的边。

65 Android利用canvas画各种图形(点、直线、弧、圆、椭圆、文字、矩形、多边形、曲线、圆角矩形) .

Android利用canvas画各种图形(点、直线、弧、圆、椭圆、文字、矩形、多边形、曲线、圆角矩形)

64 UI控件Switch的使用方法

(1)在布局中添加上Switch控件


// 以下是该控件的常用属性:
textOn:控件打开时显示的文字 
textOff:控件关闭时显示的文字 
thumb:控件开关的图片 
track:控件开关的轨迹图片 
typeface:设置字体类型 
switchMinWidth:开关最小宽度 
switchPadding:设置开关 与文字的空白距离 
switchTextAppearance:设置文本的风格 
checked:设置初始选中状态 
splitTrack:是否设置一个间隙,让滑块与底部图片分隔(API 21及以上) 
showText:设置是否显示开关上的文字(API 21及以上)

一般不会用该控件原本的样式,那么我们就需要自己修改样式了:
(2)资源文件相关
bg_thumb_on.xml



    
    
    
    
    
    
    
    

bg_thumb_off.xml



    
    
    
    
    
    
    
    

bg_track_on.xml



    
    
    
    
    
    

bg_track_off.xml



    
    
    
    
    
    

selector_switch_thumb.xml



    
    

selector_switch_track.xml



    
    

(3)MainActivity.class

public class MainActivity extends Activity{
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  
    final Switch aSwitch = (Switch) findViewById(R.id.s_v);
    aSwitch.setChecked(false);
    aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
      @Override
      public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
        // 控制开关字体颜色
        if (isChecked) { } else { }
      }
    });
  }
}

(4)效果
在这里插入图片描述
(5)学习链接
Android UI控件Switch的使用方法

65 ViewPager难点

65.1 ViewPager刷新问题详解

@Override
public int getItemPosition(@NonNull Object object) {
	if (mBannerList != null && mBannerList.size() == 1) {
	    // 参考:https://www.jianshu.com/p/266861496508
        // 解决只有一页时 notifyDataSetChanged() 页面不刷新问题的方法
        return POSITION_NONE;
    } else {
        return super.getItemPosition(object);
    }
}

ViewPager刷新问题详解

Android官网解析:PagerAdapter

65.2 ViewPager的创建下一个页面–设置当前页面数据–销毁前一个页面

/**
 * 创建指定位置的页面视图
 */
 @Override
public Object instantiateItem(@NonNull ViewGroup container, final int position) {
}
  
/**
 * 销毁一个给定位置的页面
 */
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
}
 
/**
 * 获取当前界面的ViewGroup
 */
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
}

ViewPager中获取当前界面的Fragment

ViewPager动态加载数据

66 Android之尺寸getDimension、getDimensionPixelOffset和 getDimensionPixelSize区别

(1)先通过/res/values/dimens.xml自定义数值,了解一下尺寸(Dimension)的不同用法:

  
    16px  
    16dp  
    16sp  
  

(2)执行代码测试
Android进阶之开发问题笔记_第48张图片Android进阶之开发问题笔记_第49张图片
(3)结论

  • 相同点:返回获取某个dimen的值,如果dimen单位是dp或sp,则需要将其乘以density(屏幕密度)如果单位是px,则不用
  • 不同点:
    getDimension:返回类型为float,
    getDimensionPixelSize:返回类型为int,由浮点型转成整型时,采用四舍五入原则。
    getDimensionPixelOffset:返回类型为int,由浮点型转成整型时,原则是忽略小数点部分。

(4)学习链接
Android之尺寸getDimension、getDimensionPixelOffset 和 getDimensionPixelSize

67 获取android默认的useragent

(1)定义
User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA。
(2)是什么
它是一个特殊字符串头,是一种向访问网站提供你所使用的移动设备或者浏览器类型及版本、操作系统及版本、浏览器内核、等信息的标识。
(3)什么用
通过这个标识,用户所访问的网站可以显示不同的排版从而为用户提供更好的体验或者进行信息统计;例如用手机访问谷歌和电脑访问是不一样的,这些是谷歌根据访问者的UA来判断的。
(4)UserAgent的内容

User Agent:Mozilla/5.0 (Linux; Android 4.4.4; SAMSUNG-SM-N900A Build/tt) AppleWebKit/537.36 (KHTML, like Gecko) 
Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36

(5)使用示例

String userAgent = DeviceUtils.getUserAgent();
if (TextUtils.isEmpty(userAgent)) {
	 // 考虑到初始化WebView耗时,最后才使用WebView
	userAgent = new WebView(ReaderApplication.getGlobalContext()).getSettings().getUserAgentString();
}
  /**
     * 获取UserAgent
     * @return
     */
    public static String getUserAgent() {
        String userAgent;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            try {
                // 注意:WebSettings使用系统默认的api
                userAgent = WebSettings.getDefaultUserAgent(BaseApplication.getContext());
            } catch (Exception e) {
                userAgent = System.getProperty("http.agent");
            }
        } else {
            userAgent = System.getProperty("http.agent");
        }
        if (TextUtils.isEmpty(userAgent)) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0, length = userAgent.length(); i < length; i++) {
            char c = userAgent.charAt(i);
            if (c <= '\u001f' || c >= '\u007f') {
                sb.append(String.format("\\u%04x", (int) c));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

(6)学习链接
Android获取以及修改WebView的UserAgent

获取android默认的useragent

66 Android 版本兼容 — Android 6.0 和 7.0后获取Mac地址

/**
     * 获取MAC地址
     * @param context
     * @return
     */
    public static String getMacAddress(Context context) {
        String mac;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            mac = getMacDefault(context);
        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            mac = getMacAddressM();
        } else {
            mac = getMacFromHardware();
        }
        return mac;
    }

    /**
     * Android  6.0 之前(不包括6.0)
     * 必须的权限  
     * @param context
     * @return
     */
    private static String getMacDefault(Context context) {
        String mac = "02:00:00:00:00:00";
        if (context == null) {
            return mac;
        }

        WifiManager wifi = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        if (wifi == null) {
            return mac;
        }
        WifiInfo info = null;
        try {
            info = wifi.getConnectionInfo();
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (info == null) {
            return mac;
        }

        mac = info.getMacAddress();
        if (!TextUtils.isEmpty(mac)) {
            mac = mac.toUpperCase(Locale.ENGLISH);
        }
        return mac;
    }

    /**
     * Android 6.0(包括) - Android 7.0(不包括)
     * @return
     */
    private static String getMacAddressM() {
        String wifiaddress = "02:00:00:00:00:00";
        try {
            wifiaddress = new BufferedReader(new FileReader(new File("/sys/class/net/wlan0/address"))).readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return wifiaddress;
    }


    /**
     * 遍历循环所有的网络接口,找到接口是 wlan0
     * 必须的权限 
     * @return
     */
    private static String getMacFromHardware() {
        try {
            List all = Collections.list(NetworkInterface.getNetworkInterfaces());
            for (NetworkInterface nif : all) {
                if (!nif.getName().equalsIgnoreCase("wlan0")) {
                    continue;
                }

                byte[] macBytes = nif.getHardwareAddress();
                if (macBytes == null) {
                    return "";
                }

                StringBuilder res1 = new StringBuilder();
                for (byte b : macBytes) {
                    res1.append(String.format("%02X:", b));
                }

                if (res1.length() > 0) {
                    res1.deleteCharAt(res1.length() - 1);
                }
                return res1.toString();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "02:00:00:00:00:00";
    }

68 Android Studio 3.1以后 Messages窗口在哪里看

Android进阶之开发问题笔记_第50张图片
Android stdio 3.1 Messages窗口取消啦

69 Android Studio中Make Project、Clean Project、Rebuild Project 的作用

(1)Make Project:编译Project下所有Module,一般是自上次编译后Project下有更新的文件,增量编译,不生成Apk
(2)Make Selected Modules:编译指定的Module,一般是自上次编译后Module下有更新的文件,增量编译,不生成Apk。

(3)Clean Project删除之前编译后的编译文件。部分版本的AS会自动重新编译整个Project,不生成Apk

(4)Rebuild Project先执行Clean操作,删除之前编译的编译文件和可执行文件,然后重新编译新的编译文件,不生成Apk

(5)Build APK:前面4个选项都是编译,没有生成apk文件[旧版本AS,新版本的AS会自动生成debug.apk],如果想生成Apk,需要点击Build APK。
(6)Generate Signed APK:生成有签名的Apk(一般项目嵌入第三方,生成release包时必须混淆,否则无法生成Apk)。

70 buildScript块、allprojects块、根级别的repositories区别

(1)buildScript块的repositories主要是用来加载gradle脚本自身需要使用的资源插件
比如在buildscript下dependencies依赖classpath ‘com.android.tools.build:gradle:2.2.2’,那需要在repositories中配置依赖jcenter()。

buildscript {
    repositories {
        google()
        jcenter()        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }
}

(2)allprojects块的repositories是项目本身需要的依赖
比如要依赖友盟maven库的com.umeng.umsdk:analytics:8.0.0库,那需要将maven { url ‘https://dl.bintray.com/umsdk/release’ }写在allprojects下,而不是buildscript中,不然找不到。

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
      
        // 添加 Sensors Analytics maven 库地址
        maven { url 'https://dl.bintray.com/zouyuhan/maven'  }
        // 友盟
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
// app
// 添加 com.sensorsdata.analytics.android 插件
apply plugin: 'com.sensorsdata.analytics.android'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // 友盟
    implementation 'com.umeng.umsdk:analytics:8.0.0'
    implementation 'com.umeng.umsdk:common:2.0.0'
}

(3)根级别的repositories主要是为了当前项目提供所需依赖包,比如log4j、spring-core等依赖包可从mavenCentral仓库获得。
(4)学习链接
buildscript和allprojects的作用和区别是什么?

71 SonarQube静态代码检查

(1)集成步骤
使用 Jenkins 与 Sonar 集成对代码进行持续检测

(2)

72 keystore签名

72.1 Android Studio生成keystore签名文件

(1)步骤
Android进阶之开发问题笔记_第51张图片
(2)学习链接
Android Studio生成keystore签名文件

72.2 .keyStore和.jks的签名的区别

(1).keystore是Eclipse打包生成的签名;而.jks是Android studio生成的签名。都是用来打包的,并保证应用的唯一性!这就是他们的最大的区别!
(2)备注:很多第三方市场,我们上传apk的时候,他们只支持keystore,需要我们把.jks签名转化为.keystore!

72.3 获取签名文件MD5、SHA1、SHA256值

(1)方法一:在文件夹中找到这个签名文件(.keystore或.jks),打开命令行窗口,执行

keytool -list -v -keystore debug.keystore

Android进阶之开发问题笔记_第52张图片
(2)方法二:如果电脑的命令配置不生效,直接进入java jdk的目录下,并将签名文件(.keystore或.jks)拷贝到此目录下,再执行命令。
Android进阶之开发问题笔记_第53张图片
Android进阶之开发问题笔记_第54张图片
在这里插入图片描述
(3)学习链接
Android获取签名文件sha1值

72.4 解决apk覆盖安装的时候,出现安装失败与旧版本部兼容的问题

72.4.1 问题描述

Android进阶之开发问题笔记_第55张图片

72.4.2 解决方法

(1)你需要检查你的新旧apk所使用的签名文件是否是同一个。
(2)检查你的签名文件是否是发布版本,debug签名的应用程序不能在Android Market上发布,它会强制你使用自己的签名,debug.keystore在不同的机器上所生成的可能都不一样,就意味着如果你换了机器进行 apk 版本升级,那么将会出现上面那种程序不能覆盖安装的问题。相当于软件不具备升级功能,所以一定要使用正式发布版本的签名。
(3)检查清单文件中的两个属性:versionCode和versionName,发布新版本的时候会有可能会忘记修改这两项。如果没有修改会导致软件发布后用户无法接收到更新提示,也就影响软件的更新率。

72.4.3 参考链接

Android 解决apk覆盖安装的时候,出现安装失败,与旧版本部兼容的问题

72.5 两种签名机制V1和V2的区别

(1)signature version的区别

V1:可对签名后的文件,作适当修改,并重新压缩。 
V2:不能对签名后的 APK作任何修改,包括重新解压。因为它是针对字节进行的签名,所以任何改动都会影响最终结果。

(2)v1和v2的签名正确用法

1:只勾选v1签名所有机型都能用,但是在7.0及以上不会使用更安全的验证方式; 
2:只勾选V2签名7.0以下机型会在直接安装完后显示未安装,7.0及以上机型使用V2的方式验证成功安装; 
3:同时勾选V1和V2对所有机型成功安装(建议使用).

(4)学习链接
Andriod Studio两种签名机制V1和V2的区别

Android V1、V2签名包和快速集成美团多渠道打包

72.6 jarsigner签名

jarsigner -verbose -keystore D:\project\xiaoshuting\keystore\haosou.keystore -signedjar C:\Users\Administrator\Desktop\sign\signed.apk  C:\Users\Administrator\Desktop\com.xiaomi.appstore.signature.verification.apk  qihoo

73 Activity管理

73.1 请求回调中判断当前Activity是否已关闭

(1)在网络请求回调/异步回调更新UI/定时操作更新UI,通过(!ActivityUtils.assertActivityDestroyed(this))或者(getMvpView() != null)判断当前Activity是否被销毁,再做逻辑处理或者回调。(2018/7/24)

/**
     * 弱引用Activity,判断Activity是否已经销毁
     *
     * @param activity
     * @return true=Activity已经销毁,false=没有销毁
     */
    public static boolean assertActivityDestroyed(Activity activity) {
        if (activity == null) {
            return true;
        }

        WeakReference weakReference = new WeakReference<>(activity);

        return isActivityFinished(weakReference.get());
    }

    /**
     * 直接判断Activity是否已经销毁
     *
     * @param activity
     * @return true=Activity已经销毁,false=没有销毁
     */
    public static boolean isActivityFinished(Activity activity) {
        return activity == null || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) || activity.isFinishing();
    }
UCManager.getInstance().getAssetsInfoManager().fetchWalletInfo(new IGetMyWalletListener() {
            @Override
            public void onSuccess(WalletInfo walletInfo) {
                if (ActivityUtils.assertActivityDestroyed(RechargeManagerActivity.this)) {
                    return;
                }
            }

            @Override
            public void onFail(String msg) {
                if (ActivityUtils.assertActivityDestroyed(RechargeManagerActivity.this)) {
                    return;
                }

                ToastUtils.showToast(RechargeManagerActivity.this, R.string.recharge_get_assets_error);
            }
        });

73.2 Android关闭应用程序的5种方法

(1)killProcess结束进程

android.os.Process.killProcess(android.os.Process.myPid());

android中所有的activity都在主进程中,在Androidmanifest.xml中可以设置成启动不同进程,Service不是一个单独的进程也不是一个线程。当你Kill掉当前程序的进程时也就是说整个程序的所有线程都会结束,Service也会停止,整个程序完全退出。

(2)exit结束java虚拟机

System.exit(0);

用来结束当前正在运行中的java虚拟机。如何status是非零参数,那么表示是非正常退出。当调用System.exit(0)时,杀死了整个进程,这时候活动所占的资源也会被释放。和return 相比有以下不同点:return是回到上一层,而System.exit(status)是回到最上层。进一步探索System.exit被调用后的行为,它在虚拟机在退出前会执行两个清除任务。第一,它会执行所有通过Runtime.addShutdownHook注册的shutdownhooks.它能有效的释放JVM之外的资源。第二,执行清除任务,运行相关的finalizer方法终结对象。在android手机中查看当前正在运行的进程时可以发现还可以查看"后台缓存的进程",你会发现很多退出了的程序还在后台缓存的进程中,如果不要让程序在后台缓存那么就可以用System.exit(0);来退出程序了,可以清除后台缓存的本进程,对于有些手机也是没有用的。这是退出Java程序的方法,也可以用来退出Android。

(3)根据Activity的生命周期,例如:从A->B->C->D,这时需要从D直接退出程序。

// 在D中使用下面的代码:
Intent intent = new Intent();   
intent.setClass(D.this, A.class);  
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);  // 注意本行的FLAG设置  
startActivity(intent);  
finish(); // 关掉自己 
// 在A中加入代码: 
protected void onNewIntent(Intent intent) {  
	super.onNewIntent(intent);  
	// 退出  
	if ((Intent.FLAG_ACTIVITY_CLEAR_TOP & intent.getFlags()) != 0) {  
   		 finish();  
    }  
}  
A的Manifest.xml配置成android:launchMode="singleTop" 

总结:一般A是程序的入口点,从D起一个A的activity,加入标识Intent.FLAG_ACTIVITY_CLEAR_TOP这个过程中会把栈中B,C都清理掉。因为A是android:launchMode=“singleTop” ,不会调用onCreate(),而是响应onNewIntent()。这时候判断Intent.FLAG_ACTIVITY_CLEAR_TOP,然后把A finish()掉。 栈中A,B,C,D全部被清理,所以整个程序退出了。
学习链接:采用FLAG_ACTIVITY_CLEAR_TOP退出整个程序(多activity)

(4)finish()将活动推向后台

finish();

finish是Activity的类,仅仅针对Activity,当调用finish()时,只是将活动推向后台,并没有立即释放内存,活动的资源并没有被清理;

(5)比较常用的方法,使用Activity栈管理Activity

public class ActivityManager {
    private static ActivityManager sInstance;
    private Stack mActivityStack;
  
    /**
     * 单例
     *
     * @return
     */
    public static ActivityManager getAppManager() {
        if (sInstance == null) {
            synchronized (ActivityManager.class) {
                if (sInstance == null) {
                    sInstance = new ActivityManager();
                }
            }
        }
        return sInstance;
    }

    /**
     * 添加Activity到堆栈
     * @param activity
     */
    public void addActivity(Activity activity) {
        if (mActivityStack == null) {
            mActivityStack = new Stack<>();
        }
        mActivityStack.add(activity);
    }

    /**
     * 获取当前Activity(堆栈中最后一个压入的)
     * @return
     */
    public Activity currentActivity() {
        if (mActivityStack == null) {
            return null;
        }
        return mActivityStack.lastElement();
    }

    /**
     * 结束指定的Activity
     * @param activity
     */
    public void finishActivity(Activity activity) {
        if (mActivityStack == null || activity == null) {
            return;
        }
        mActivityStack.remove(activity);
        activity.finish();
    }

    /**
     * 结束当前Activity(堆栈中最后一个压入的)
     */
    public void finishActivity() {
        if (mActivityStack == null) {
            return;
        }
        // 获取到当前Activity
        Activity activity = mActivityStack.lastElement();
        // 结束指定Activity
        finishActivity(activity);
    }

    /**
     * 结束指定类名的Activity
     * @param cls
     */
    public void finishActivity(Class cls) {
        if (mActivityStack == null) {
            return;
        }
        List activities = new ArrayList<>();
        for (Activity activity : mActivityStack) {
            if (activity.getClass().equals(cls)) {
                // finishActivity(activity);
                activities.add(activity);
            }
        }

        // 结束所有类名相同activity
        mActivityStack.removeAll(activities);
        for (Activity activity : activities) {
            finishActivity(activity);
        }
    }

    /**
     * 结束所有Activity
     */
    public void finishAllActivity() {
        if (mActivityStack == null) {
            return;
        }

        for (int i = 0, size = mActivityStack.size(); i < size; i++) {
            if (null != mActivityStack.get(i)) {
                Activity activity = mActivityStack.get(i);
                if (!activity.isFinishing()) {
                    activity.finish();
                }
            }
        }
        mActivityStack.clear();
    }

    /**
     * 退出应用程序
     * 这里关闭的是所有的Activity,没有关闭Activity之外的其他组件;
     * 

* android.os.Process.killProcess(android.os.Process.myPid()) * 杀死进程关闭了整个应用的所有资源,有时候是不合理的,通常是用堆栈管理Activity; *

* System.exit(0)杀死了整个进程,这时候活动所占的资源也会被释放,它会执行所有通过Runtime.addShutdownHook注册的shutdown hooks. * 它能有效的释放JVM之外的资源,执行清除任务,运行相关的finalizer方法终结对象, *

* 而finish只是退出了Activity。 */ public void AppExit() { try { finishAllActivity(); // DalvikVM的本地方法 // 杀死该应用进程:android.os.Process.killProcess(android.os.Process.myPid()); // System.exit(0):这些方法如果是放到主Activity就可以退出应用,如果不是主Activity就是退出当前的Activity } catch (Exception e) { e.printStackTrace(); } } }

(6)学习链接
Android 关闭应用程序的6种方法

74 底部的控件设置bottom,部分手机不生效

(1)在底部控件设置bottom—>部分手机不生
Android进阶之开发问题笔记_第56张图片
(2)在父布局设置paddingBottom—>兼容所有手机
这里写图片描述

75 启动页最佳适配方案,避免拉伸

(1)设置主题XstSplashTheme

 
            
                

                
            
        

(2)XstSplashTheme

// values\xst_xtyles.xml


// v21\xst_xtyles.xml

 

// v28\xst_xtyles.xml


(3)splash_bg.xml:使用layer-list叠加的方式–学习shape和selector和layer-list的使用




    
    

    
    

        
    

(4)SplashActivity.java设置全屏显示+设置适配刘海屏全屏展示,主要是:华为小米等厂商的8.1刘海屏

(5)activity_splash.xml布局(使用logo图适配,不要使用带图案的全屏图)




    
    

    

       
        
    

Android进阶之开发问题笔记_第57张图片

ic_logo.png
Android进阶之开发问题笔记_第58张图片

76 沉浸式相关

76.1 fitsSystemWindow作用

(1)概念理解-layout属性
用于根据系统窗口(如状态栏)调整视图布局。 如果为true,保留actionbar,title,屏幕的底部虚拟按键,为系统窗口留出空间。如果为false,移除actionbar,title,屏幕的底部虚拟按键,将整个屏幕当作可用的空间。 仅在此视图处于非嵌入活动中时才会生效。
(2)android:fitsSystemWindows=“true”
Android进阶之开发问题笔记_第59张图片
(3)android:fitsSystemWindows=“false”
Android进阶之开发问题笔记_第60张图片
(4)小结:页面实现沉浸式状态栏(即状态栏隐藏),需要设置android:fitsSystemWindows=“false”;默认的页面则是android:fitsSystemWindows=“true”。
(5)学习链接:fitsSystemWindow作用

76.2 实现变色状态栏–状态栏与导航栏颜色相同

(1)开源库地址
niorgai/StatusBarCompat
(2)使用

allprojects {
	repositories {
		...
		maven { url "https://jitpack.io" }
	}
}
dependencies {
	// After AndroidX
	implementation ('com.github.niorgai:StatusBarCompat:2.3.0', {
	    exclude group: 'androidx.appcompat:appcompat'
	    exclude group: 'com.google.android.material:material'
	})
	// Before AndroidX
	compile ('com.github.niorgai:StatusBarCompat:2.1.4', {
	    exclude group: 'com.android.support'
	})
}
// 在BaseActivity设置,适配所有Activity
public abstract class BaseActivity extends SuperFragmentActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());

        if (isShowStatusBar()) {
            StatusBarCompat.setStatusBarColor(this, ContextCompat.getColor(this, R.color.global_theme_red));
        }
    }

    protected boolean isShowStatusBar() {
        return true;
    }
}

// 在需要不设置默认颜色的子Activity,重写isShowStatusBar(),返回false
public class SplashActivity extends BaseActivity {
    
    @Override
    protected boolean isShowStatusBar() {
        return false;
    }
}

76.3 实现沉浸式状态栏

(1)对于AppCompat主题中,各个颜色属性的含义,可以参考下图
Android进阶之开发问题笔记_第61张图片


(2)在布局父布局中设置:android:fitsSystemWindows=“false”




(3)在Activity的super.onCreate(savedInstanceState)后设置setWindowStatus

public class WindowUtils {
    /**
     * 设置Window实现沉浸式状态栏
     */
    public static void setWindowStatus(Window window) {
        if (window == null) {
            return;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
        }
    }
}
public class MainActivity extends BaseActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WindowUtils.setWindowStatus(getWindow());
	
        initView();
    }
}

(4)实现效果
Android进阶之开发问题笔记_第62张图片
(5)学习链接
Android沉浸式状态栏、变色状态栏、透明状态栏、修改状态栏颜色及透明

Android状态栏微技巧,带你真正理解沉浸式模式

76.4 动态设置控件的margin以及padding值

(1)工具类

/**
 * 状态栏工具类
 */
public class StatusBarUtils {
    /**
     * 设置ActionBar状态
     * @param context
     * @param layout
     * @param isSetPadding
     */
    public static void setActionBarParams(Context context, View layout, boolean isSetPadding) {
        int statusBarHeight = (int) DeviceUtils.getStatusBarHeight(context);
        ViewGroup.LayoutParams params = layout.getLayoutParams();
        if (isSetPadding) {
            // 设置内边距-状态栏高度
            layout.setPadding(0, statusBarHeight, 0, 0);
        }
        // 设置ActionBar高度为原高度+状态栏高度
        params.height += statusBarHeight;
        layout.setLayoutParams(params);
    }

    /**
     * 设置边距Padding
     * @param view
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    public static void setPaddingParams(View view, int left, int top, int right, int bottom) {
        if (view != null) {
            ViewGroup.LayoutParams params = view.getLayoutParams();
            view.setPadding(left, top, right, bottom);
            view.setLayoutParams(params);
        }
    }

    /**
     * 设置边距Margin
     * @param view
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    public static void setMarginsParams(View view, int left, int top, int right, int bottom) {
        if (view != null && view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
            ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
            p.setMargins(left, top, right, bottom);
            view.requestLayout();
        }
    }
}

(2)在52中MainActivity设置WindowUtils.setWindowStatus(getWindow()),书架Fragment占领状态栏,保证标题栏正常显示,如下图:
Android进阶之开发问题笔记_第63张图片

public class BookShelfFragment extends BaseTabFragment {
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
       mRootView = inflater.inflate(R.layout.fragment_book_shelf, container, false);
       setActionBarParams(mRootView);
    }

   /**
     * 设置ActionBar状态
     * @param view
     */
    private void setActionBarParams(View view) {
        if (!isShowStatusBar()) {
            return;
        }

        View actionbarHead = view.findViewById(R.id.actionbar_head);
        StatusBarUtils.setActionBarParams(getActivity(), actionbarHead, true);
    }
}

(3)学习链接
在代码中设置leftMargin和rightMargin值

Android动态设置控件大小以及设定margin以及padding值

77 LeakCanary2的免写"初始化代码"原理

(1)基于ContentProvider这个对于绝大数开发来说,基本没用到的四大组件之一来做的。查看他的leakcanary-leaksentry模块的AndroidManifest.xml文件:


    
  

LeakSentryInstaller

internal class LeakSentryInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    CanaryLog.logger = DefaultCanaryLog()
    val application = context!!.applicationContext as Application
    InternalLeakSentry.install(application)  
    // 骚操作在这里,利用系统自动调用CP的onCreate方法来做初始化
    return true
  }
}

(2)好处:带来了 “免侵入” ,不需要业务人员写任何代码。因为一般启动的顺序是:

Application -> attachBaseContext ==>ContentProvider->onCreate ==>Application->onCreate ==>Activity->onCreate

,所以对于大多数场景,写在CP的初始化的实际是足够优先了。

(3)官网
LeakCanary Overview官网

LeakCanary Overview Github

(4)参考链接
LeakCanary2的免写"初始化代码"原理

78 xml资源文件中@、@android:type、@*、?、@+含义和区别

78.1 @代表引用资源

(1)引用自定义资源。格式:@[package:]type/name

android:text="@string/hello"

(2)引用系统资源。格式:@android:type/name

android:textColor="@android:color/opaque_red"

(3)注意:@android:type/name是@[package:]type/name 的一个子类

78.2 @*代表引用系统的非public资源。格式:@*android:type/name

注意:没在public.xml中声明的资源是google不推荐使用的。

78.3 @+代表在创建或引用资源 。格式:@+type/name

@+id/资源ID名,新建一个资源ID
@id/资源ID名,应用现有已定义的资源ID,包括系统ID
@android:id/资源ID名,引用系统ID,其等效于@id/资源ID名

android:id="@+id/selectdlg"
android:id="@android:id/text1"
android:id="@id/button3"  

78.4 ?代表引用主题属性(只能在style资源和XML属性中使用)

(1)attr/: 引用应用内的属性资源
①在values/文件夹下建一个attrs文件,在这里保存style属性,通过?attr/来引用,比如在 attr.xml里定义了一个属性:



    
    

②在Theme下指定值:


    

③在layout中通过引用的方式去使用这个属性了:

android:background="?attr/fillColorAttr"

(4)?android: 引用系统内建的属性资源

style="?android:attr/progressBarStyleHorizontal"
style="?android:progressBarStyleHorizontal"

78.5 学习链接

@android, ?attr/ 和 ?android 的区别

Android xml资源文件中@、@android:type、@*、?、@+含义和区别

Android根据资源名获取资源ID

79 App全局置灰如何实现?

79.1 App页面置灰需求背景

当前新冠肺炎疫情正在全球蔓延,逝世人数众多。政府政府规定今年的 4 月 4 号为哀悼日,所有互联网项目能置灰的要跟随置灰处理。淘宝、京东等部分 App 都显示为灰色,对抗击新冠肺炎疫情斗争牺牲烈士和逝世同胞的深切哀悼。在 Android 开发中如何实现全局置灰是开发人员需要解决的技术问题。

79.2 方案1

最简单的实现方式是,在App内置2套切图及对应的2套色值资源文件等,根据全局置灰开关,选择加载不同的图片和色值。这种方案全局实现起来工作繁琐、工作量大。且会导致 App包增大很多,用户体验差,不是一种很好的实现方案。

79.3 方案2

实现灰度化的思路应该从Android 系统界面绘制原理出发寻找实现方案。系统是通过Paint将内容绘制到界面上的,Paint中可以设置ColorMatrix ,界面置灰可以通过通过使用颜色矩阵(ColorMatrix)来实现。

在Android中,系统使用一个颜色矩阵ColorMatrix来处理图像的色彩效果,通过这个类方便地改变矩阵值来处理颜色效果。ColorMatrix提供了以下关键方法进行对颜色的处理:

①setRotate(int axis, float degree) :设置颜色的色调,第一个参数,系统分别使用0、1、2 来代表 Red、Green、Blue 三种颜色的处理;而第二个参数,就是需要处理的值。
②setSaturation(float sat):设置颜色的饱和度,参数代表设置颜色饱和度的值,当饱和度为0时,图像就变成灰度图像。
③setScale(float rScale, float gScale, float bScale , float aScale):对于亮度进行处理。

实现去色重点用到了ColorMatrix类中的setSaturation方法,查看官网API对这个方法有这么一段说明:“Set the matrix to affect the saturation of colors. A value of 0 maps the color to gray-scale. 1 is identity。 ” 意思是去设置颜色矩阵的饱和度,0是灰色的,1是原图。通过调用setSaturation (0) 方法即可将图像置为灰色 。通过继承ImageView实现该方案验证,具体代码如下:

public class GrayImageView extends AppCompatImageView {
    private Paint mPaint;

    public GrayImageView(Context context) {
        super(context, null);
    }

    public GrayImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        // 一个颜色矩阵 ColorMatrix,来处理图像的色彩效果。通过这个类,可以很方便地改变矩阵值来处理颜色效果
        ColorMatrix colorMatrix = new ColorMatrix();
        // 设置颜色的饱和度,参数代表设置颜色饱和度的值,当饱和度为0时,图像就变成灰度图像;1是原图。
        colorMatrix.setSaturation(0);
        mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
        super.onDraw(canvas);
    }
}

Android进阶之开发问题笔记_第64张图片

79.4 方案优化

从上面ImageView的置灰实现猜测是否所有的View都能给置灰,DecorView是Activity窗口的根视图,根视图通过ColorMatrix设置置灰应该可以在全部子元素有效。在BaseActivity 中添加代码如下:

    private void putAsh() {
        Paint paint = new Paint();
        ColorMatrix colorMatrix = new ColorMatrix();
        colorMatrix.setSaturation(0f);
        paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
        // 一般选择硬件绘制还是软件绘制会调用下面的API:
        getWindow().getDecorView().setLayerType(View.LAYER_TYPE_HARDWARE, paint);
    }

Android进阶之开发问题笔记_第65张图片

79.5 学习链接

在 Android 开发中,App 全局置灰如何实现?

App 黑白化实现探索2, 发现了一种更方便的方案

80 lass.isAssignableFrom与instanceof的区别

(1)isAssignableFrom

假设有两个类Class1和Class2, Class1.isAssignableFrom(Class2)表示:

①类Class1和Class2是否相同;

②Class1是否是Class2的父类或接口,调用者和参数都是java.lang.Class类型;

(2)instanceof

用来判断一个对象是否是一个类及其子类或接口及其子接口的的实例;

格式:object instanceof TypeName // 第一个参数是对象实例名,第二个参数是具体的类名或接口名

(3)学习链接
lass.isAssignableFrom与instanceof的区别

你可能感兴趣的:(Android进阶)