内容简介:或者
- Android 操作系统是一个多用户 Linux 操作系统,每个应用都是一个用户
- 操作系统一般会给每个应用分配一个唯一的 Linux 用户 ID,这个 ID 对应用是不可见的。但有些情况下两个应用可以共享同一个 Linux 用户 ID,此时他们可以访问彼此的文件,甚至还可以运行在同一个 Linux 进程中,共享同一个虚拟机。但两个应用的签名必须是一样的
- 每个进程都有自己的虚拟机,一般每个应用都运行在自己的 Linux 进程中
应用组件
- 应用没有唯一的入口,没有
main()
函数,因为应用是由多个组件拼凑在一起的,每个组件都是系统或者用户进入应用的入口,组件之间既可以是相互独立的,也可以是相互依赖的。系统和其它应用在被允许的情况下可以启动/激活一个应用的任意一个组件 - 组件有四种类型:
Activity
,Service
,BroadcastReceiver
和ContentProvider
Activity
-
Activity
表示一个新的用户界面,只能由系统进行创建和销毁,应用只能监听到一些生命周期回调,这些回调通常也被叫作生命周期方法 -
Activity
的名字一旦确定好就不要再更改了,否则可能会引发一系列问题Service
-
Service
表示一个后台服务,Service
可以是独立的,可以在应用退出后继续运行。也可以绑定到其他进程 /Activity
,表示其他进程想使用这个Service
,像输入法、动态壁纸、屏保等系统功能都是以Service
的形式存在的,在需要运行的时候进行绑定 - 大部分情况下,建议使用
JobScheduler
,因为JobScheduler
和Doze
API 配合下一般会比简单使用Service
更省电BroadcastReceiver
-
BroadcastReceiver
是一个事件传递的组件,通过它应用可以响应系统范围的广播通知。系统的包管理器会在安装应用时将应用中的静态广播接收器注册好,所以即使应用没在运行,系统也能把事件传递到该组件。 - 通过
BroadcastReceiver
可以实现进程间通信ContentProvider
-
ContentProvider
是在多个应用间共享数据的组件,如果应用的一些数据想要被其它应用使用,必须通过ContentPrivider
进行管理,不过应用的私有数据也可以通过ContentProvider
进行管理,主要还是因为ContentProvider
即提供了共享数据的抽象,使用者不需要知道数据究竟是以文件形式还是数据库等其他形式存储的,只需要通过ContentProvider
提供的 统一的 API 进行数据的增删改查即可。同时ContentProvider
还提供了 安全 环境,可以根据需要方便地控制数据的访问权限,不需要手动控制文件权限或数据库权限 - 为了安全,也为了方便,一般需要通过
ContentResolver
操作ContentProvider
- 通过
ContentProvider
可以实现进程间通信激活组件
- 应用不能也不应该直接激活其它应用的任意一个组件,但是系统可以,所以要想激活一个组件,需要给系统发一个消息详细说明你的意图( Intent ),之后系统就会为你激活这个组件
-
Activity
,Service
,BroadcastReceiver
都需要通过被称为Intent
的异步消息激活 - 被激活组件返回的结果也是
Intent
形式的 -
ContentProvider
只有在收到ContentResolver
的请求时才会被激活 - 只有
BroadcastReceiver
可以不在 manifest 文件中注册,因为有些BroadcastReceiver
需要在程序运行时动态地注册和注销。而其它组件必须在 manifest 文件中注册,否则无法被系统记录,也就无法被激活 - 如果
Intent
通过组件类名显式指明了唯一的目标组件,那么这个Intent
就是显式地,否则就是隐式的。隐式Intent
一般只描述要执行动作的类型,必要时可以携带数据,系统会根据这个隐式Intent
的描述决定激活哪个组件,如果有多个组件符合激活条件,系统一般会弹出选择框让用户选择到底激活哪个组件 -
Service
必须使用显式Intent
激活,不能声明IntentFilter
- 启动指定的
Activity
使用显式Intent
,启动随便一个能完成指定工作的Activity
使用隐式Intent
。能完成指定工作的那些想要被隐式Intent
激活的Activity
需要事先声明好IntentFilter
表示自己有能力处理什么工作,IntentFilter
一般通过 能完成的动作 、意图类型 和 额外数据 来描述 - 要想被隐式
Intent
激活,意图类型至少要包含android.intent.category.DEFAULT
的意图类型 - 在使用隐式
Intent
激活Activity
之前一定要检查一下有没有Activity
能处理这个Intent
:if (sendIntent.resolveActivity(getPackageManager()) != null) { startActivity(sendIntent); }
或者
PackageManager packageManager = getPackageManager(); List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); boolean isIntentSafe = activities.size() > 0;
-
使用隐式
Intent
时每次都强制用户选择一个组件激活:Intent intent = new Intent(Intent.ACTION_SEND); String title = getResources().getString(R.string.chooser_title); Intent chooser = Intent.createChooser(intent, title); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(chooser); }
-
如果想要你的
Activity
能被隐式Intent
激活,如果想要某个 链接 能直接跳转到你的Activity
,必须配置好IntentFilter
。这种链接分为两种: Deep links 和 Android App Links -
Deep links对链接的 scheme 没有要求,对系统版本也没有要求,也不会验证链接的安全性,不过需要一个
android.intent.action.VIEW
的 action 以便 Google Search 能直接打开,需要android.intent.category.DEFAULT
的 category 才能响应隐式 Intent,需要android.intent.category.BROWSABLE
的 category 浏览器打开链接时才能跳转到应用,所以经典用例如下。一个 intent filter 最好只声明一个 data 描述,否则你得考虑和测试所有变体的情况。系统处理这个链接的流程为: 如果用户之前指定了打开这个链接的默认应用就直接打开这个应用 → 如果只有一个应用可以处理这个链接就直接打开这个应用 → 弹窗让用户选择用哪个应用打开<activity android:name="com.example.android.GizmosActivity" android:label="@string/title_gizmos" > <intent-filter android:label="@string/filter_view_http_gizmos"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "http://www.example.com/gizmos” --> <data android:scheme="http" android:host="www.example.com" android:pathPrefix="/gizmos" /> <!-- note that the leading "/" is required for pathPrefix--> </intent-filter> <intent-filter android:label="@string/filter_view_example_gizmos"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "example://gizmos” --> <data android:scheme="example" android:host="gizmos" /> </intent-filter> </activity>
-
Android App Links是一种特殊的 Deep links,要求链接必须是你自己网站的 HTTP URL 链接,系统版本至少是 Android 6.0 (API level 23),优点是安全且具体,其他应用不能使用你的链接,不过你得先 验证你的链接 ,由于链接和网站链接一致所以可以无缝地在应用和网站间切换,可以支持 Instant App,可以通过浏览器、谷歌搜索 APP、系统屏幕搜索、甚至 Google Assistant 的链接直接跳转到应用。验证链接的流程为: 将
<intent-filter>
标签的android:autoVerify
设置为true
以告诉系统自动验证你的应用属于这个 HTTP URL 域名 → 填写好网站域名和应用 ID 并使用签名文件生成 Digital Asset Links JSON 文件 → 将文件上传到服务器,访问路径为https://domain.name/.well-known/assetlinks.json
,响应格式为application/json
,子域名也需要存在对应的文件,一个域名可以关联多个应用,一个应用也可以关联多个域名,且可以使用相同的签名 → 利用编辑器插件完成关联并验证应用资源
- 添加资源限定符的顺序为: SIM 卡所属的国家代码和移动网代码 → 语言区域代码 → 布局方向 → 最小宽度 → 可用宽度 → 可用高度 → 屏幕大不大 → 屏幕长不长 → 屏幕圆不圆 → 屏幕色域宽不宽 → 屏幕支持的动态范围高不高 → 屏幕方向 → 设备的 UI 模式 → 夜间模式 → 屏幕像素密度 → 触摸屏类型 → 键盘类型 → 主要的文字输入方式 → 导航键是否可用 → 主要的非触摸导航方式 → 支持的 API level
- 一个资源目录的每种资源限定符最多只能出现一次
- 必须提供缺省的资源文件
- 资源目录名是大小写不敏感的
-
drawable 资源取别名:
<?xml version="1.0" encoding="utf-8"?> <resources> <drawable name="icon">@drawable/icon_ca</drawable> </resources>
-
布局文件取别名:
<?xml version="1.0" encoding="utf-8"?> <merge> <include layout="@layout/main_ltr"/> </merge>
-
只有动画、菜单、raw 资源 以及 xml/ 目录中的资源不能使用别名
- 寻找使用最优资源的流程:
- 在应用程序运行时,设备的配置可能会发生变化(如屏幕方向变化、切换到多窗口模式,切换了系统语言),默认情况下系统会销毁重建正在运行的
Activity
,所以应用程序必须保证销毁重建的过程中用户的数据和页面状态完好无损地恢复。如果不想系统销毁重建你的Activity
只需要在 manifest 文件的<activity>
标签的android:configChanges
属性中添加你想自己处理的配置更改,多个配置使用 “|
“ 隔开,此时系统就不会在这些配置更改后销毁重建你的这个Activity
而是直接调用它的onConfigurationChanged()
回调方法,你需要在这个回调中自己处理配置更改后的行为。 -
Activity
的销毁重建不但发生在设备配置更改后,只要用户离开了某个Activity
,那么那个Activity
就随时可能被系统销毁。所以销毁重建是无法避免的,也不应该逃避,而是应该想办法保存和恢复状态 - 由于各种各样的硬件都能安装 Android 操作系统,Android 操作系统之间也可能千差万别,而应用程序的一些功能是与这些软硬件息息相关的,如拍照应用需要设备必须有摄像头才能正常工作。应用可以通过
<uses-feature>
标签声明只有满足这些软硬件要求的设备才能安装,通过它的android:required
属性设置该要求是不是必须的,程序中可以通过PackageManager.hasSystemFeature()
方法判断核心知识
Activity 相关
生命周期方法
- 当
Activity
变得对用户可见时,将会回调onStart()
, 当Activity
变得可以和用户交互时,将会回调onResume()
-
onPause()
被调用时Activity
可能依然对用户全部可见,如多窗口模式下没有获得焦点时,所以在onResume()
中申请资源在onPause()
中释放资源的想法并不总是合理的 -
onStop()
被调用时表示Activity
已经完全不可见了,此时应该尽量停止包含动画在内的 UI 更新,尽量释放暂时不用的资源。对于 stopped 的Activity
,系统随时可能杀掉包含这个Activity
的进程,如果没有合适的机会可以在onStop()
中保存一些数据 - 如果系统在未经用户允许的情况下销毁了
Activity
(杀掉了该Activity
实例所在的进程),那么系统肯定记得这个实例存在过,在用户重新回到这个Activity
时会重新创建一个新的实例,并将之前保存好的实例状态传递给这个新的实例。这个系统之前保存好的用来恢复Activity
状态的数据被称为 实例状态 (Instance state),实例状态是以键值对的形式存储在 Bundle 对象中的,默认系统只能自动存储和恢复有 ID 的 View 的简单状态(如输入框的文本,滚动控件的滚动位置),但由于在主线程中序列化或反序列化Bundle
对象既消耗时间又消耗系统进程内存,所以最好只用它保存简单、轻量的数据 -
onSaveInstanceState()
被调用的时机: 对于Build.VERSION_CODES.P
及之后的系统该方法会在onStop()
之后随时可能被调用,对于之前的系统该方法会在onStop()
之前随时被调用 -
onRestoreInstanceState()
被调用的时机: 如果有实例状态要恢复那么一定会在onStart()
之后被调用 -
onActivityResult()
被调用时机:onResume()
之前。目标Activity
没有显式返回任何结果或者崩溃那么 resultCode 就会是RESULT_CANCELED
任务和返回栈
-
Activity
可以在 manifest 文件中定义自己应该如何与当前任务相关联,Activity
也可以在启动其它Activity
时通过Intent
的 flag 要求其它Activity
应该如何与当前任务相关联,如果两者同时出现,那么Intent
的 flag 要求获胜 -
launchMode
属性默认是standard
,每次启动这样的Activity
都会新建一个新的实例放入启动它的任务中。 一个新的 Intent 总会创建一个新的实例。一个任务可以有多个该 Activity 的实例,每个该 Activity 的实例可以属于不同的任务 -
launchMode
属性是singleTop
的Activity
: 如果当前任务顶部已经是这个Activity
的实例那么就直接将Intent
传递给这个实例的onNewIntent()
方法。 一个任务可以有多个该 Activity 的实例,每个该 Activity 的实例可以属于不同的任务 -
launchMode
属性是singleTask
的Activity
: 如果这个Activity
的实例已经在某个任务中存在了那么就直接将Intent
传递给这个实例的onNewIntent()
方法,并将其所在的任务移到前台即当前任务顶部,否则会新建一个任务并实例化一个这个Activity
的实例放在栈底 -
launchMode
属性是singleInstance
的Activity
: 和singleTask
类似,不过它会保证新的任务中有且仅有一个这个Activity
的实例 -
FLAG_ACTIVITY_NEW_TASK
: 行为和singleTask
一样,不过在新建任务之前会先寻找是否已经存在和这个Activity
有相同 affinity 的任务,如果已经存在就不新建任务了,而是直接在那个任务中启动 -
FLAG_ACTIVITY_SINGLE_TOP
: 行为和singleTop
一样 -
FLAG_ACTIVITY_CLEAR_TOP
: 如果当前任务中已经有要启动的Activity
的实例了,那么就销毁它上面所有的Activity
( 甚至包括它自己 ),由于launchMode
属性是standard
的Activity
一个新的 Intent 总会创建一个新的实例,所以如果要启动的Activity
的launchMode
属性是standard
的并且没有FLAG_ACTIVITY_SINGLE_TOP
的 flag,那么这个 flag 会 销毁它自己 然后创建一个 新的实例 -
FLAG_ACTIVITY_CLEAR_TOP
和FLAG_ACTIVITY_NEW_TASK
结合使用可以直接定位指定的Activity
到前台 - 不管要启动的
Activity
是在当前任务中启动还是在新任务中启动,点击返回键都可以直接或间接回到之前的Activity
,间接的情况像singleTask
是将整个任务而不是只有一个Activity
移到前台,任务中的所有的Activity
在点击返回键的时候都要依次弹出 - 如果离开了任务,系统可能会清除任务中除了最底层
Activity
外的的所有Activity
。将最底层Activity
的<activity>
标签的alwaysRetainTaskState
属性设置为true
可以保留任务中所有的Activity
。将最底层Activity
的<activity>
标签的clearTaskOnLaunch
属性设置为true
可以在无论何时进入或离开这个任务都清除任务中除了最底层Activity
外的的所有Activity
。包含最底层Activity
在内的任何Activity
只要finishOnTaskLaunch
属性设置为true
那么离开任务再回来都不会出现了 - 将
Activity
作为新文档添加到最近任务中需要设置newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
且launchMode
必须是standard
的,如果此时又设置了newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
那么系统每次都会创建新的任务并将目标Activity
作为根Activity
,如果没有设置FLAG_ACTIVITY_MULTIPLE_TASK
,那么Activity
实例会被重用到新的任务中(如果已经存在这样的任务就不会重建,而是直接将任务移到前台并调用onNewIntent()
) -
<activity>
标签的android:documentLaunchMode
属性默认是none
: 不会为新文档创建新的任务。intoExisting
与设置了FLAG_ACTIVITY_NEW_DOCUMENT
但没设置FLAG_ACTIVITY_MULTIPLE_TASK
一样。always
与设置了FLAG_ACTIVITY_NEW_DOCUMENT
同时设置了FLAG_ACTIVITY_MULTIPLE_TASK
一样。never
和none
一样不过会覆盖FLAG_ACTIVITY_NEW_DOCUMENT
和FLAG_ACTIVITY_MULTIPLE_TASK
- 使用
Intent.FLAG_ACTIVITY_NEW_DOCUMENT|android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
同时<activity>
标签的android:autoRemoveFromRecents
属性设置为false
可以让文档Activity
即使结束了也可以保留在最近任务中 - 使用
finishAndRemoveTask()
方法可以移除当前任务动态申请权限
- Android 6.0 (API level 23) 开始 targetSdkVersion >= 23 的应用必须在运行时动态申请权限
- 权限请求对话框是操作系统进行管理的,应用无法也不应该干预。
- 系统对话框描述的是权限组而不是某个具体权限。
- 如果用户授予了权限组中的一个权限,那么再申请该权限组的其它权限时系统会自动授予,不需要用户再授权。但这并不意味着该权限组中的其它权限就不用申请了,因为权限处于哪个权限组将来有可能会发生变化。
- 调用
requestPermissions()
并不意味着系统一定会弹出权限请求对话框,也就是说不能假设调用该方法后就发生了用户交互,因为如果用户之前勾选了 “禁止后不再询问” 或者系统策略禁止应用获取权限,那么系统会直接拒绝此次权限请求,没有任何交互。 - 如果某个权限跟应用的主要功能无关,如应用中广告可能需要位置权限,用户可能很费解,此时在申请权限之前弹出对话框向用户解释为什么需要这个权限是个不错的选择。但不要在所有申请权限之前都弹出对话框解释,因为频繁地打断用户的操作或让用户进行选择容易让用户不耐烦。
-
Fragment
中的onRequestPermissionsResult()
方法只有在使用Fragment#requestPermissions()
方法申请权限时才可能接收到回调,建议将权限放在所属Activity
中申请和处理。private void showContactsWithPermissionsCheck() { if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_CONTACTS)) { // TODO: 弹框解释为什么需要这个权限. 【下一步】 -> 再次请求权限 } else { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_CONTACTS}, RC_CONTACTS); } } else { showContacts(); } } private void showContacts() { startActivity(ContactsActivity.getIntent(MainActivity.this)); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case RC_CONTACTS: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { showContacts(); } else { if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_CONTACTS)) { // TODO: 弹框引导用户去设置页主动授予该权限. 【去设置】 -> 应用信息页 } else { // TODO: 弹框解释为什么需要这个权限. 【下一步】 -> 再次请求权限 } } break; default: break; } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == RC_SETTINGS) { // TODO: 在用户主动授予权限后重新检查权限,但不要在这里进行事务提交等生命周期敏感操作 } }
Shortcut
- 类似于 iOS 的 3D Touch,长按启动图标弹出几个快捷入口,入口最好不要超过 4 个,像搜索、扫描二维码、发帖等应用程序最常用功能的入口被称为静态 shortcut,不会随着用户不同或随着用户使用而改变。还有一种像从某个存档点继续游戏、任务进度等与用户相关的上下文敏感入口被称为动态 shortcut,会因用户不同或随着用户使用不断变化。还有一种在 Android 8.0 (API level 26) 及以上系统版本上像固定网页标签等用户主动固定到桌面的快捷方式被称为固定 shortcut
- 静态 shortcut 系统可以自动备份和恢复,动态 shortcut 需要应用自己备份和恢复,固定 shortcut 的图标系统无法备份和恢复因此需要应用自己完成
-
android:shortcutId
和android:shortcutShortLabel
属性是必须的,android:shortcutShortLabel
不能超过 10 个字符,android:shortcutLongLabel
不能超过 25 个字符,android:icon
不能包含 tint - 获取
ShortcutManager
的方式有两个:getSystemService(ShortcutManager.class)
和getSystemService(Context.SHORTCUT_SERVICE)
- 创建固定 shortcut:
ShortcutManager mShortcutManager = context.getSystemService(ShortcutManager.class); if (mShortcutManager.isRequestPinShortcutSupported()) { ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(context, "my-shortcut").build(); Intent pinnedShortcutCallbackIntent = mShortcutManager.createShortcutResultIntent(pinShortcutInfo); PendingIntent successCallback = PendingIntent.getBroadcast(context, 0, pinnedShortcutCallbackIntent, 0); mShortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.getIntentSender()); }
其它
-
Parcelable
对象用来在进程间、Activity
间传递数据,保存实例状态也是用它,不过最好只存储和传递少量数据,最好别超过 50k,否则既可能影响性能又可能导致崩溃 - Android 9 (API level 28) 开始废弃了
Loader
API,包括LoaderManager
和CursorLoader
等类的使用。推荐使用ViewModel
和LiveData
在Activity
或Fragment
生命周期中加载数据 -
Activity
可以通过getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
保持屏幕常亮,这是最推荐、最简单、最安全的保持屏幕常亮的方法,给 view 添加android:keepScreenOn="true"
也是一样的。这个只在这个Activity
生命周期内有效,所以大可放心,如果想提前解除常亮,只需要清除这个 flag 即可 -
WAKE_LOCK
可以阻止系统睡眠,保持 CPU 一直运行,需要android.permission.WAKE_LOCK
权限,通过powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag")
创建实例,通过wakeLock.acquire()
方法请求锁,通过wakelock.release()
释放锁 -
WakefulBroadcastReceiver
结合IntentService
也可以阻止系统睡眠UI 相关
系统栏适配
- Android 4.1 (API level 16) 开始可以通过
setSystemUiVisibility()
方法在各个 view 层次中(一般是在 DecorView 中)配置 UI flag 实现系统栏(状态栏、导航栏统称)配置,最终汇总体现到 window 级 -
View.SYSTEM_UI_FLAG_FULLSCREEN
可以隐藏状态栏,View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
可以隐藏导航栏。 但是: 用户的任何交互包括触摸屏幕都会导致 flag 被清除导航栏 保持可见 ,一旦离开当前Activity
flag 就会被清除,所以如果在onCreate()
方法中设置了这个 flag 那么按 HOME 键再回来状态栏又保持可见了,非要这样设置的话一般要放在onResume()
或onWindowFocusChanged()
方法中,而且这样设置只有在目标 View 可见时才会生效,状态栏/导航栏的显示隐藏会导致显示内容的大小尺寸跟着变化。 -
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
可以让内容显示在状态栏后面,View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
可以让内容显示在导航栏后面,这样无论系统栏显示还是隐藏内容都不会跟着变化,但不要让可交互的内容出现在系统栏区域内,通过将android:fitsSystemWindows
属性设置为true
可以让父容器调整 padding 以便为系统栏留出空间,如果想自定义这个 padding 可以通过覆写fitSystemWindows(Rect insets)
方法完成 - lean back 全屏模式:
View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
,隐藏状态栏和导航栏,任何交互都会清除 flag 使系统栏 保持可见 - Immersive 全屏模式:
View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
,隐藏状态栏和导航栏,从被隐藏的系统栏边缘向内滑动会使系统栏 保持可见 ,应用无法响应这个手势 - sticky immersive 全屏模式:
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
,隐藏状态栏和导航栏,从被隐藏的系统栏边缘向内滑动会使系统栏 暂时可见 ,flag 不会被清除,且系统栏的背景是半透明的,会覆盖应用的内容,应用也可以响应这个手势,在用户没有任何交互或者没有系统栏交互几秒钟后系统栏会 自动隐藏 - 真正的沉浸式全屏体验需要 6 个 flag:
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN
-
监听系统栏可见性(sticky immersive 全屏模式无法监听):
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { @Override public void onSystemUiVisibilityChange(int visibility) { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { // TODO: The system bars are visible. Make any desired } else { // TODO: The system bars are NOT visible. Make any desired } } });
-
全面屏适配只需要指定支持的最大宽高比即可:
<meta-data android:name="android.max_aspect" android:value="2.4"/>
- Android 9 (API level 28) 开始支持刘海屏 cutout 的配置,window 的属性
layoutInDisplayCutoutMode
默认是LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
,竖屏时可以渲染到刘海区,横屏时不允许渲染到刘海区。LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
横竖屏都可以渲染到刘海区。LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
横竖屏都不允许渲染到刘海区,可以在values-v28/styles.xml
文件中通过android:windowLayoutInDisplayCutoutMode
指定默认的刘海区渲染模式 - 华为手机通过
<meta-data android:name="android.notch_support" android:value="true" />
属性声明应用是否已经适配了刘海屏,如果没适配,那么在横屏或者竖屏不显示状态栏时会禁止渲染到刘海区,开发者文档: 《华为刘海屏手机安卓O版本适配指导》 - 小米手机通过
<meta-data android:name="notch.config" android:value="portrait|landscape" />
设置默认的刘海区渲染模式,开发者文档: 《小米刘海屏 Android O 适配》 , 《小米刘海屏 Android P 适配》 - 其他手机的开发者文档有: OPPO 手机的 《OPPO凹形屏适配说明》 ,VIVO 手机的 《异形屏应用适配指南》 ,锤子手机的 《Smartisan 开发者文档》
- Android 5.0 (API level 21) 开始支持通过 window 的
setStatusBarColor()
方法设置状态栏背景色,要求 window 必须添加WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
的 flag 并且清除WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
的 flag - Android 6.0 (API level 23) 开始可以通过
setSystemUiVisibility()
方法设置View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
flag 兼容亮色背景的状态栏,同样要求 window 必须添加WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
的 flag 并且清除WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
的 flag - 小米手机在 MIUI 开发版 7.7.13 之前需要通过反射兼容亮色背景的状态栏,开发者文档: 《MIUI 9 & 10“状态栏黑色字符”实现方法变更通知》
- 魅族手机同样需要通过反射兼容亮色背景的状态栏,开发者文档: 《状态栏变色》
动画
- view 动画系统 只能作用于 view 对象,只能改变 view 的部分样式,只是简单改变了 view 绘制,并没有改变 view 真正的位置和属性。核心类是
android.view.animation.Animation
和它的ScaleAnimation
等子类,一般使用AnimationUtils.loadAnimation()
方法加载。不建议使用,除非为了方便又能满足现在和将来的需求 - 属性动画系统 是一个健壮的、优雅的动画系统,可以对任意对象的属性做动画。核心类是
android.animation.Animator
的子类ValueAnimator
、ObjectAnimator
、AnimatorSet
- 通过调用
ValueAnimator
的ofInt()
、ofFloat()
等工厂方法获取ValueAnimator
对象,通过它的addUpdateListener()
方法可以监听动画值并在里面进行自定义操作 -
ObjectAnimator
作为ValueAnimator
的子类可以自动地为目标对象的命名属性设置动画,但是对目标对象有严格的要求: 目标对象必须有对应属性的 setter 方法,如果在工厂方法中只提供了一个动画值那么它会作为终止值,起始值为目标对象的当前值,此时为了获取当前属性值目标对象必须有对应属性的 getter 方法。有些属性的更改不会导致 view 重新渲染,此时需要主动调用invalidate()
方法强制触发重绘 -
AnimatorListenerAdapter
提供了Animator.AnimatorListener
接口的空实现 - 多数情况下可以直接使用系统提供的几个动画 duration,如
getResources().getInteger(android.R.integer.config_shortAnimTime)
- 可以调用任意 view 对象的
animate()
方法获取ViewPropertyAnimator
对象,链式调用这个对象的scaleX()
、alpha()
等方法可以简单方便地同时对 view 的多个属性做动画 - 为了更好地重用和管理属性动画,最好使用 XML 文件来描述动画并放到
res/animator/
目录下,ValueAnimator
对应<animator>
,ObjectAnimator
对应<objectAnimator>
,AnimatorSet
对应<set>
,使用AnimatorInflater.loadAnimator()
可以加载这些动画 - 动态 Drawable 的实现有两种,最传统最简单的就是像电影关键帧一样依次指定关键帧和每一帧的停留时间,
AnimationDrawable
对应于 XML 文件中的<animation-list>
,保存目录为res/drawable/
,AnimationDrawable
的start()
方法可以在onStart()
中调用。还有一种是AnimatedVectorDrawable
,需要res/drawable/
中的<animated-vector>
引用res/drawable/
中的<vector>
对其使用res/animator/
中的<objectAnimator>
动画 - 突然更改显示的内容会让视觉感受非常突兀不和谐,而且可能意识不到哪些内容突然变了,所以很多场景下需要使用动画过渡一下,而不是突然更改显示的内容
- 显示隐藏 view 的常用动画有三个: crossfade 动画,card flip 动画,circular reveal 动画
- crossfade 动画就是内容淡出另一个内容淡入交叉进行,也被称为 溶入 动画。实现方式为: 事先将淡入 view 的 visibility 设置为
GONE
→ 开始动画时将淡入 view 的 alpha 设置为 0,visibility 设置为VISIBLE
→ 将淡入 view 的 alpha 动画到 1,将淡出 view 的 alpha 动画到 0 并在动画结束时将淡出 view 的 visibility 设置为GONE
- card flip 动画就是卡片翻转动画,需要四个动画描述:
card_flip_right_in
,card_flip_right_out
,card_flip_left_in
,card_flip_left_out
- Android 5.0 (API level 21) 开始支持 circular reveal 圆形裁剪动画,实现方式为: 事先将 view 的 visibility 设置为
INVISIBLE
→ 利用ViewAnimationUtils.createCircularReveal()
方法创建半径从 0 到Math.hypot(cx, cy)
的圆形裁剪动画 → 将 view 的 visibility 设置为VISIBLE
然后开启动画 - 直线 动画移动 view 只需要借助
ObjectAnimator.ofFloat()
方法动画设置 view 的translationX
或translationY
属性即可 - 曲线 动画移动 view 还需要借助 Android 5.0 (API level 21) 开始提供的
PathInterpolator
插值器(对应于 XML 文件中的<pathInterpolator>
),他需要个Path
对象描述运动的贝塞尔曲线。可以使用ObjectAnimator.ofFloat(view, "translationX", 100f)
同时设置PathInterpolator
也可以直接设置 view 动画路径ObjectAnimator.ofFloat(view, View.X, View.Y, path)
。系统提供的fast_out_linear_in.xml
、fast_out_slow_in.xml
、linear_out_slow_in.xml
三个基础的曲线插值器可以直接使用 - 基于物理的动画需要引用
support-dynamic-animation
支持库,最常见的就是FlingAnimation
和SpringAnimation
动画,物理动画主要是模拟现实生活中的物理世界,利用经典物理学的知识和原理实现动画过程,其中最关键的就是 力 的概念。FlingAnimation
就是用户通过手势给动画元素一个力,动画元素在这个力的作用下运动,之后由于摩擦力的存在慢慢减速直到结束,当然这个力也可以通过程序直接指定(指定固定的初始速度)。SpringAnimation
就是弹簧动画,动画元素的运动与弹簧有关 -
FlingAnimation
通过setStartVelocity()
方法设置初始速度,通过setMinValue()
和setMaxValue()
约束动画值的范围,通过setFriction()
设置摩擦力(如果不设置默认为 1)。如果动画的属性不是以像素为单位的,那么需要通过setMinimumVisibleChange()
方法设置用户可察觉到动画值的最小更改,如对于TRANSLATION_X
,TRANSLATION_Y
,TRANSLATION_Z
,SCROLL_X
,SCROLL_Y
1 像素的更改就对用户可见了,而对于ROTATION
,ROTATION_X
,ROTATION_Y
最小可见更改是MIN_VISIBLE_CHANGE_ROTATION_DEGREES
即 1/10 像素,对于ALPHA
最小可见更改是MIN_VISIBLE_CHANGE_ALPHA
即 1/256 像素,对于SCALE_X
和SCALE_Y
最小可见更改是MIN_VISIBLE_CHANGE_SCALE
即 1/500 像素,计算公式为: 自定义属性值的范围 / 动画的变化像素范围。 -
SpringAnimation
需要先巩固一下弹簧的知识,弹簧有一个属性叫 阻尼比 ζ (damping ratio),是实际的粘性阻尼系数 C 与临界阻尼系数 Cr 的比。ζ = 1 时为临界阻尼,这是最小的能阻止系统震荡的情况,系统可以最快回到平衡位置。0 < ζ < 1 时为欠阻尼,物体会作对数衰减振动。ζ > 1 时为过阻尼,物体会没有振动地缓慢回到平衡位置。ζ = 0 表示不考虑阻尼,震动会一直持续下去不会停止。默认是SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
即 0.5,可以通过getSpring().setDampingRatio()
设置。弹簧另一个属性叫 刚度 (stiffness),指弹框的弹性,刚度越大形变产生的力就越大,默认是SpringForce.STIFFNESS_MEDIUM
即 1500.0,可以通过getSpring().setStiffness()
设置
-
FlingAnimation
和SpringAnimation
动画通过setStartVelocity()
设置固定的初始速度时最好用 dp/s 转成 px/s :TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics())
,用户手势的初始速度可以通过GestureDetector.OnGestureListener
或VelocityTracker
计算 -
SpringAnimation
动画使用start()
方法开始动画时属性值不会马上变化,而是在每次动画脉冲即绘制之前更改。animateToFinalPosition()
方法会马上设置最终的属性值,如果动画没开始就开始动画,这在链式依赖的弹簧动画中非常有用。cancel()
方法可以结束动画在其当前位置,skipToEnd()
方法会跳转至终止值再结束动画,可以通过canSkipToEnd()
方法判断是否是阻尼动画 - 放大预览动画只需要同时动画更改目标 view 的
X
,Y
,SCALE_X
,SCALE_Y
属性即可,不过要先计算好两个 view 最终的位置和初始缩放比 - Android 提供了预加载的布局改变动画,可以通过
android:animateLayoutChanges="true"
属性告诉系统开启默认动画,或者通过LayoutTransition
API 设置 - Activity 内部的布局过渡动画: 过渡动画框架可以在开始
Scene
和结束Scene
开始过渡动画,Scene
存储着 view hierarchy 状态,包括所有 view 和其属性值,开始Scene
可以通过setExitAction()
定义过渡动画开始前要执行的操作,结束Scene
可以通过Scene.setEnterAction()
定义过渡动画完成后要执行的操作。如果 view hierarchy 是静态不变的,可以通过布局文件描述和加载Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this)
,否则可以手动创建new Scene(mSceneRoot, mViewHierarchy)
。Transition
的内置子类包括AutoTransition
、Fade
、ChangeBounds
,可以在res/transition/
目录下定义内置的<fade xmlns:android="http://schemas.android.com/apk/res/android" />
,多个组合包裹在<transitionSet>
标签中,然后使用TransitionInflater.from(this).inflateTransition(R.transition.fade_transition)
加载。还可以手动创建new Fade()
。开始过渡动画时只需要执行TransitionManager.go(mEndingScene, mFadeTransition)
即可。默认是对Scene
中所有的 view 作动画,可以通过addTarget()
或removeTarget()
在开始过渡动画前进行调整。如果不想在两个 view hierarchy 间进行过渡,而是在同一个 view hierarchy 状态更改后执行过渡动画,那就不需要使用Scene
了,先利用TransitionManager.beginDelayedTransition(mRootView, mFade)
让系统记录 view 的更改,然后增删 view 来更改 view hierarchy 的状态,系统会在重绘 UI 时执行延迟过渡动画。由于SurfaceView
由非 UI 线程更新,所以它的过渡可能有问题,TextureView
在一些过渡类型上可能有问题,AdapterView
与过渡动画框架不兼容,TextView
的大小过渡动画可能有问题 - Activity 之间的过渡动画: 需要 Android 5.0 (API level 21) ,内置的进入退出过渡动画包括: explode 从中央进入或退出,slide 从一边进入或退出,fade 透明度渐变进入或退出。内置的共享元素过渡动画包括: changeBounds 动态更改目标 view 的边界,changeClipBounds 动态裁剪目标 view 的边界,changeTransform 动态更改目标 view 的缩放和旋转,changeImageTransform 动态更改目标 view 的缩放和尺寸。过渡动画需要两个 Activity 都要开启 window 的内容过渡,通过
android:windowActivityTransitions
属性设置为true
或者手动getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
,通过setExitTransition()
和setSharedElementExitTransition()
方法可以为起始 Activity 设置退出过渡动画,通过setEnterTransition()
和setSharedElementEnterTransition()
方法可以为目标 Activity 设置进入过渡动画。激活目标 Activity 的时候需要携带ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
的 Bundle,返回的时候要使用Activity.finishAfterTransition()
方法。共享元素需要使用android:transitionName
属性或者View.setTransitionName()
方法指定名字,多个共享元素使用Pair.create(view1, "agreedName1")
传递信息 - 自定义过渡动画需要继承
Transition
,实现captureStartValues()
和captureEndValues()
方法捕获过渡的 view 属性值并告诉过渡框架,具体实现为通过transitionValues.view
检索当前 view,通过transitionValues.values.put(PROPNAME_BACKGROUND, view.getBackground())
存储属性值,为了避免冲突 key 的格式必须为package_name:transition_name:property_name
。同时还要实现createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)
方法,框架调用这个方法的次数取决于开始和结束 scene 需要更改的元素数 - 动画可能会影响性能,必要时可以启用 Profile GPU Rendering 进行调试
其它
- Android 8.0 (API level 26) 开始支持自适应启动图标,自适应启动图标必须由前景和背景两部分组成,尺寸必须都是 108 x 108 dp,其中内部的 72 x 72 dp 用来显示图标,靠近四个边缘的 18 dp 是保留区域,用来进行视觉交互
- 对于字体大小自适应的
TextView
宽和高都不能是wrap_content
,autoSizeTextType
默认是none
,设置为uniform
开启自适应,默认最小12sp
,最大112sp
,粒度1px
。autoSizePresetSizes
属性可以设置预置的一些大小 - Android 8.0 (API level 26) 开始支持 XML 自定义字体,兼容库可以兼容到 Android 4.1 (API level 16),字体文件路径为
res/font/
,使用属性为fontFamily
,获取Typeface
为getResources().getFont(R.font.myfont);
,兼容库使用ResourcesCompat.getFont(context, R.font.myfont)
- Android 9 (API level 28) 支持控件放大镜功能,
Magnifier
的show()
方法的参数是相对于被放大 View 的左上角的坐标 - 工程中的 Drawable 资源只能有一个状态,你 不应该手动更改它的任何属性 ,否则会影响到其它使用这个 Drawable 资源的地方
- Android 7.0 (API level 24) 开始支持在 XML 文件中使用自定义 Drawable,公共顶级类使用全限定名作为标签名即可
<com.myapp.MyDrawable>
,公共静态内部类可以使用 class 属性class="com.myapp.MyTopLevelClass$MyDrawable"
- Android 5.0 (API level 21) 开始支持为 Drawable 设置 tint
- Android 5.0 (API level 21) 开始支持矢量图,支持库可以支持到 Android 2.1 (API level 7+),兼容低版本是需要 Gradle 插件版本大于 2.0+ 时添加
vectorDrawables.useSupportLibrary = true
并使用VectorDrawableCompat
和AnimatedVectorDrawableCompat
BroadcastReceiver 相关
- Android 9 (API level 28) 开始
NETWORK_STATE_CHANGED_ACTION
广播不再包含 SSID,BSSID 等信息 - Android 8.0 (API level 26) 开始限制应用注册一些静态隐式
BroadcastReceiver
,免除这项限制的广播包括ACTION_LOCKED_BOOT_COMPLETED
等 不太可能影响用户体验的广播 - Android 7.0 (API level 24) 开始不能发送
ACTION_NEW_PICTURE
和ACTION_NEW_VIDEO
系统广播,不能注册CONNECTIVITY_ACTION
的静态广播 - 应该尽量在代码中动态注册注销
BroadcastReceiver
-
onReceive()
方法中不能进行复杂工作否则会导致 ANR,onReceive()
方法一旦执行完,系统可能就认为这个广播接收器已经没用了,随时会杀掉包含这个广播接收器的进程,包括这个进程启动的线程。使用goAsync()
方法可以在PendingResult.finish()
执行前为广播接收器的存活争取更多的时间,但最好还是使用JobScheduler
等方式进行长时间处理工作 - 使用
sendBroadcast()
方法发的广播属于常规广播,所有能接收这个广播的广播接收器接收到广播的顺序是不可控的 - 使用
sendOrderedBroadcast()
方法发的广播属于有序广播,根据广播接收器的优先级一个接一个地传递这条广播,相同优先级的顺序不可控,广播接收器可以选择继续传递给下一个,也可以选择直接丢掉 - 使用
LocalBroadcastManager.getInstance(this).sendBroadcast()
方法发的广播属于应用进程内的本地广播,这样的广播只有应用自己知道,比系统级的全局广播更有效率 - 为了保证广播的 action 全局唯一,action 的名字最好使用应用的包名作为前缀,最好声明成静态字符串常量
数据存储与共享
存储方式
- 系统会在 安装应用 时在 内部存储器 的文件系统中为应用生成一个私有文件目录,一般是
/data/data/your.application.package/
或data/user/0/your.application.package/
。这个目录除了系统和应用自己谁都无法访问,除非拥有权限。可以通过getFilesDir()
方法获取这个路径表示,可以通过openFileOutput(filename, Context.MODE_PRIVATE)
写这个目录下的文件。当 卸载应用 时这个目录也会被删除。这个目录有个特殊子目录cache/
目录,用来存储临时缓存文件,系统可能会在存储空间不足时清理这个目录,可以通过getCacheDir()
方法获取这个路径表示,可以通过File.createTempFile(fileName, null, context.getCacheDir())
在这个目录下创建一个临时文件。还有个特殊的子目录shared_prefs/
目录,用来以 XML 文件的形式存储简单的键值对数据,需要使用SharedPreferences
API 进行管理 - 读写外存(外存是指可以被移除的 外部存储器 )文件需要先动态申请
READ_EXTERNAL_STORAGE
或WRITE_EXTERNAL_STORAGE
权限,然后检查外存是否可用,Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
表示可写,Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)
表示可读。使用new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), albumName)
可以读写公有外存目录的文件,使用new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), albumName)
可以读写私有外存目录的文件,私有外存目录也会在 卸载应用 时被删除。通过getExternalFilesDirs()
方法可以列出所有的外存目录。 - 使用
myFile.delete()
或myContext.deleteFile(fileName)
删除文件 - 直接使用 SQLite API 进行数据库操作既麻烦又容易出错,建议使用 Room 等其它 ORM 库进行数据库操作
- 获取
SharedPreferences
的方式有三个: 通过PreferenceManager.getDefaultSharedPreferences()
可以获取或创建名字为context.getPackageName() + "_preferences"
模式为Context.MODE_PRIVATE
的文件。通过MainActivity.this.getPreferences(Context.MODE_PRIVATE)
可以获取或创建名字为当前Activity
类名的文件。使用context.getSharedPreferences("file1", Context.MODE_PRIVATE)
可以获取或创建名字是 file1 的文件。MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
从 Android 7.0 (API level 24) 开始被 禁止使用 了。commit()
方法会将数据同步写到磁盘所以可能会阻塞 UI,而apply()
方法会异步写到磁盘。分享文件
- 为了安全地共享文件,分享的文件必须通过 content URI 表示,必须授予这个 content URI 临时访问权限。
FileProvider
作为ContentProvider
的特殊子类,它的getUriForFile()
方法可以为文件生成 content URI。<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.myapp.fileprovider" android:grantUriPermissions="true" android:exported="false"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" /> </provider>
<paths> <files-path path="images/" name="myimages" /> </paths>
-
android:authorities
属性一般是以当前应用包名为前缀的字符串,用来标志数据的所有者,多个的话用分号隔开 -
<root-path/>
代表根目录 -
<files-path/>
代表getFilesDir()
-
<cache-path/>
代表getCacheDir()
-
<external-path/>
代表Environment.getExternalStorageDirectory()
-
<external-files-path>
代表getExternalFilesDir(null)
-
<external-cache-path>
代表getExternalCacheDir()
-
<external-media-path>
代表getExternalMediaDirs()
File imagePath = new File(getFilesDir(), "images"); File newFile = new File(imagePath, "default_image.jpg"); Uri contentUri = getUriForFile(getContext(), "com.example.myapp.fileprovider", newFile);
-
给 Intent 添加
FLAG_GRANT_READ_URI_PERMISSION
或FLAG_GRANT_WRITE_URI_PERMISSION
的 flag 授予对这个 content URI 的临时访问权限,该权限会被目标Activity
所在应用的其它组件继承,会在所在的任务结束时自动撤销授权 - 调用
Context.grantUriPermission(package, Uri, mode_flags)
方法也可以授予FLAG_GRANT_READ_URI_PERMISSION
或FLAG_GRANT_WRITE_URI_PERMISSION
权限,但只有再调用revokeUriPermission()
方法后或者重启系统后才会撤销授权mResultIntent.setDataAndType( fileUri, getContentResolver().getType(fileUri)); MainActivity.this.setResult(Activity.RESULT_OK, mResultIntent);
Uri returnUri = returnIntent.getData(); try { mInputPFD = getContentResolver().openFileDescriptor(returnUri, "r"); } catch (FileNotFoundException e) { e.printStackTrace(); Log.e("MainActivity", "File not found."); return; } FileDescriptor fd = mInputPFD.getFileDescriptor();
ContentProvider
-
ContentProvider
的数据形式和关系型数据库的表格数据类似,因此 API 也像数据库一样包含增删改查(CRUD)操作,但为了更好地组织管理一个或多个ContentProvider
,最好通过ContentResolver
操作ContentProvider
- 对于
ContentProvider
的增删改查操作, 不能直接在 UI 线程上执行 -
Uri
和ContentUris
类的静态方法可以方便地构造 content URISELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
mCursor = getContentResolver().query( UserDictionary.Words.CONTENT_URI, mProjection, mSelectionClause, mSelectionArgs, mSortOrder);
- 为了防止 SQL 注入,禁止拼接 SQL 语句,如 mSelectionClause 不能直接包含 selectionArgs 参数值
-
ContentProvider
所在应用本身的组件则可以随便访问它 - 如果
ContentProvider
的应用不指定任何权限,那么其它应用就无法访问这个ContentProvider
的数据 - 使用者需要事先通过
<uses-permission>
标签获取访问权限 - 创建
ContentProvider
需要继承ContentProvider
并实现增删改查等一系列方法:onCreate()
在系统创建 provider 后马上调用,可以在这里创建数据库,但不要在这里做耗时操作。getType()
返回 content URI 的 MIME 类型。query()
、insert()
、update()
、delete()
进行增删改查。除了onCreate()
方法其它方法必须要保证是线程安全的其它
- Android 7.0 (API level 24) 开始禁止使用 file URI 进行文件共享
- Android 7.1.1 (API level 25) 开始安装 APK 时必须申请
Manifest.permission.REQUEST_INSTALL_PACKAGES
权限,数据必须通过FileProvider
形式共享,数据类型是application/vnd.android.package-archive
,必须给 Intent 添加FLAG_GRANT_READ_URI_PERMISSION
权限小技巧
-
测试 Deep links:
adb shell am start -W -a android.intent.action.VIEW -d "example://gizmos" com.example.android
-
测试 Android App Links:
adb shell am start -a android.intent.action.VIEW \ -c android.intent.category.BROWSABLE \ -d "http://domain.name:optional_port"
-
应用安装完 20s 后获取所有应用的链接处理策略:
adb shell dumpsys package domain-preferred-apps
-
模拟系统杀掉应用进程:
adb shell am kill com.some.package
-
.nomedia
文件会导致其所在目录不被 Media Scanner 扫描到模板代码
系统栏适配
/** * 华为手机刘海屏适配 * * @author frank * @see <a href="https://developer.huawei.com/consumer/cn/devservice/doc/50114">《华为刘海屏手机安卓O版本适配指导》</a> */ public class HwNotchSizeUtil { private static final int FLAG_NOTCH_SUPPORT = 0x00010000; /** * 是否是刘海屏手机 * * @param context Context * @return true:刘海屏 false:非刘海屏 */ public static boolean hasNotchInScreen(Context context) { boolean ret = false; try { ClassLoader cl = context.getClassLoader(); Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen"); ret = (boolean) get.invoke(HwNotchSizeUtil); } catch (Exception e) { e.printStackTrace(); } return ret; } /** * 获取刘海尺寸 * * @param context Context * @return int[0]值为刘海宽度 int[1]值为刘海高度 */ public static int[] getNotchSize(Context context) { int[] ret = new int[]{0, 0}; try { ClassLoader cl = context.getClassLoader(); Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("getNotchSize"); ret = (int[]) get.invoke(HwNotchSizeUtil); } catch (Exception e) { e.printStackTrace(); } return ret; } /** * 设置应用窗口在华为刘海屏手机使用刘海区 * * @param window Window */ public static void setFullScreenWindowLayoutInDisplayCutout(Window window) { if (window == null) { return; } WindowManager.LayoutParams layoutParams = window.getAttributes(); try { Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx"); Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class); Object layoutParamsExObj = con.newInstance(layoutParams); Method method = layoutParamsExCls.getMethod("addHwFlags", int.class); method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT); } catch (Exception e) { e.printStackTrace(); } } /** * 设置应用窗口在华为刘海屏手机不使用刘海区显示 * * @param window Window */ public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) { if (window == null) { return; } WindowManager.LayoutParams layoutParams = window.getAttributes(); try { Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx"); Constructor con = layoutParamsExCls.getConstructor(ViewGroup.LayoutParams.class); Object layoutParamsExObj = con.newInstance(layoutParams); Method method = layoutParamsExCls.getMethod("clearHwFlags", int.class); method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT); } catch (Exception e) { e.printStackTrace(); } } }
/** * 小米手机刘海屏适配 * * @author frank * @see <a href="https://dev.mi.com/console/doc/detail?pId=1293">《小米刘海屏 Android O 适配》</a> * @see <a href="https://dev.mi.com/console/doc/detail?pId=1341">《小米刘海屏 Android P 适配》</a> */ public class XiaomiNotchSizeUtil { private static final int FLAG_NOTCH_OPEN = 0x00000100; private static final int FLAG_NOTCH_PORTRAIT = 0x00000200; private static final int FLAG_NOTCH_LANDSCAPE = 0x00000400; /** * 是否是刘海屏手机 * * @param context Context * @return true:刘海屏 false:非刘海屏 */ public static boolean hasNotchInScreen(Context context) { boolean ret = false; try { ret = "1".equals(getSystemProperty("ro.miui.notch")); } catch (Exception e) { e.printStackTrace(); } return ret; } /** * 获取刘海尺寸 * * @param context Context * @return int[0]值为刘海宽度 int[1]值为刘海高度 */ public static int[] getNotchSize(Context context) { int[] ret = new int[]{0, 0}; try { int widthResId = context.getResources().getIdentifier("notch_width", "dimen", "android"); if (widthResId > 0) { ret[0] = context.getResources().getDimensionPixelSize(widthResId); } int heightResId = context.getResources().getIdentifier("notch_height", "dimen", "android"); if (heightResId > 0) { ret[1] = context.getResources().getDimensionPixelSize(heightResId); } } catch (Exception e) { e.printStackTrace(); } return ret; } /** * 横竖屏都绘制到耳朵区 * * @param window Window */ public static void setFullScreenWindowLayoutInDisplayCutout(Window window) { if (window == null) { return; } try { Method method = Window.class.getMethod("addExtraFlags", int.class); method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE); } catch (Exception e) { e.printStackTrace(); } } /** * 横竖屏都不会绘制到耳朵区 * * @param window Window */ public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) { if (window == null) { return; } try { Method method = Window.class.getMethod("clearExtraFlags", int.class); method.invoke(window, FLAG_NOTCH_OPEN | FLAG_NOTCH_PORTRAIT | FLAG_NOTCH_LANDSCAPE); } catch (Exception e) { e.printStackTrace(); } } private static String getSystemProperty(String key) { String ret = null; BufferedReader bufferedReader = null; try { Process process = Runtime.getRuntime().exec("getprop " + key); bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; StringBuilder stringBuilder = new StringBuilder(); while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line); } ret = stringBuilder.toString(); } catch (Exception e) { e.printStackTrace(); } finally { if (bufferedReader != null) { try { bufferedReader.close(); } catch (Exception e) { e.printStackTrace(); } } } return ret; } }
/** * OPPO手机刘海屏适配 * * @author frank * @see <a href="https://open.oppomobile.com/wiki/doc#id=10159">《OPPO凹形屏适配说明》</a> */ public class OppoNotchSizeUtil { /** * 是否是刘海屏手机 * * @param context Context * @return true:刘海屏 false:非刘海屏 */ public static boolean hasNotchInScreen(Context context) { return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism"); } }
/** * VIVO手机刘海屏适配 * * @author frank * @see <a href="https://dev.vivo.com.cn/documentCenter/doc/103">《异形屏应用适配指南》</a> */ public class VivoNotchSizeUtil { private static final int MASK_NOTCH_IN_SCREEN = 0x00000020; private static final int MASK_ROUNDED_IN_SCREEN = 0x00000008; /** * 是否是刘海屏手机 * * @param context Context * @return true:刘海屏 false:非刘海屏 */ public static boolean hasNotchInScreen(Context context) { boolean ret = false; try { ClassLoader cl = context.getClassLoader(); Class FtFeature = cl.loadClass("android.util.FtFeature"); Method get = FtFeature.getMethod("isFeatureSupport", int.class); ret = (boolean) get.invoke(FtFeature, MASK_NOTCH_IN_SCREEN); } catch (Exception e) { e.printStackTrace(); } return ret; } }
/** * 锤子手机刘海屏适配 * * @author frank * @see <a href="https://resource.smartisan.com/resource/61263ed9599961d1191cc4381943b47a.pdf">《Smartisan 开发者文档》</a> */ public class SmartisanNotchSizeUtil { private static final int MASK_NOTCH_IN_SCREEN = 0x00000001; /** * 是否是刘海屏手机 * * @param context Context * @return true:异形屏 false:非异形屏 */ public static boolean hasNotchInScreen(Context context) { boolean ret = false; try { ClassLoader cl = context.getClassLoader(); Class DisplayUtilsSmt = cl.loadClass("smartisanos.api.DisplayUtilsSmt"); Method get = DisplayUtilsSmt.getMethod("isFeatureSupport", int.class); ret = (boolean) get.invoke(DisplayUtilsSmt, MASK_NOTCH_IN_SCREEN); } catch (Exception e) { e.printStackTrace(); } return ret; } }
获取联系人列表
public class ContactsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener { private final static String[] FROM_COLUMNS = {ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}; private final static int[] TO_IDS = {R.id.text1}; private static final String[] PROJECTION = { ContactsContract.Contacts._ID, ContactsContract.Contacts.LOOKUP_KEY, ContactsContract.Contacts.DISPLAY_NAME_PRIMARY }; private ListView mContactsList; private SimpleCursorAdapter mCursorAdapter; public ContactsFragment() { } public static ContactsFragment newInstance() { ContactsFragment fragment = new ContactsFragment(); Bundle args = new Bundle(); fragment.setArguments(args); return fragment; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_contacts, container, false); mContactsList = rootView.findViewById(R.id.list); mCursorAdapter = new SimpleCursorAdapter( getContext(), R.layout.contact_list_item, null, FROM_COLUMNS, TO_IDS, 0); mContactsList.setAdapter(mCursorAdapter); mContactsList.setOnItemClickListener(this); return rootView; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); LoaderManager.getInstance(this).initLoader(0, null, this); } @NonNull @Override public Loader<Cursor> onCreateLoader(int i, @Nullable Bundle bundle) { return new CursorLoader(getActivity(), ContactsContract.Contacts.CONTENT_URI, PROJECTION, null, null, null); } @Override public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) { mCursorAdapter.swapCursor(cursor); } @Override public void onLoaderReset(@NonNull Loader<Cursor> loader) { mCursorAdapter.swapCursor(null); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- nginx 核心知识100讲笔记(一)
- nginx 核心知识100讲笔记(二)
- nginx 核心知识100讲笔记(三)
- elasticsearch学习笔记(三)——Elasticsearch的核心概念
- 《DeepLearning.ai 深度学习核心笔记》发布,黄海广博士整理
- 【愣锤笔记】嗯,真香!精简ES函数式编程核心概念
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。