main()
函数,由于应用是由多个组件拼凑在一块儿的,每一个组件都是系统或者用户进入应用的入口,组件之间既能够是相互独立的,也能够是相互依赖的。系统和其它应用在被容许的状况下能够启动/激活一个应用的任意一个组件Activity
,Service
,BroadcastReceiver
和 ContentProvider
Activity
表示一个新的用户界面,只能由系统进行建立和销毁,应用只能监听到一些生命周期回调,这些回调一般也被叫做生命周期方法Activity
的名字一旦肯定好就不要再更改了,不然可能会引起一系列问题Service
表示一个后台服务,Service
能够是独立的,能够在应用退出后继续运行。也能够绑定到其余进程或 Activity
,表示其余进程想使用这个 Service
,像输入法、动态壁纸、屏保等系统功能都是以 Service
的形式存在的,在须要运行的时候进行绑定JobScheduler
,由于 JobScheduler
和 Doze
API 配合下通常会比简单使用 Service
更省电BroadcastReceiver
是一个事件传递的组件,经过它应用能够响应系统范围的广播通知。系统的包管理器会在安装应用时将应用中的静态广播接收器注册好,因此即便应用没在运行,系统也能把事件传递到该组件。BroadcastReceiver
能够实现进程间通讯ContentProvider
是在多个应用间共享数据的组件,若是应用的一些数据想要被其它应用使用,必须经过 ContentPrivider
进行管理,不过应用的私有数据也能够经过 ContentProvider
进行管理,主要仍是由于 ContentProvider
提供了共享数据的抽象,使用者不须要知道数据到底是以文件形式仍是数据库等其余形式存储的,只须要经过 ContentProvider
提供的 统一的 API 进行数据的增删改查便可。同时 ContentProvider
还提供了 安全 环境,能够根据须要方便地控制数据的访问权限,不须要手动控制文件权限或数据库权限ContentResolver
操做 ContentProvider
ContentProvider
能够实现进程间通讯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 Linksandroid.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>
复制代码
<intent-filter>
标签的 android:autoVerify
设置为 true
以告诉系统自动验证你的应用属于这个 HTTP URL 域名 → 填写好网站域名和应用 ID 并使用签名文件生成 Digital Asset Links JSON 文件 → 将文件上传到服务器,访问路径为 https://domain.name/.well-known/assetlinks.json
,响应格式为 application/json
,子域名也须要存在对应的文件,一个域名能够关联多个应用,一个应用也能够关联多个域名,且可使用相同的签名 → 利用编辑器插件完成关联并验证Intent#parseUri()
方法,获取的 intent 必须严格过滤,intent 至少包含 addCategory(“android.intent.category.BROWSABLE”)
,setComponent(null)
,setSelector(null)
3 个策略<?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>
复制代码
Activity
,因此应用程序必须保证销毁重建的过程当中用户的数据和页面状态无缺无损地恢复。若是不想系统销毁重建你的 Activity
只须要在 manifest 文件的 <activity>
标签的 android:configChanges
属性中添加你想本身处理的配置更改,多个配置使用 "|
" 隔开,此时系统就不会在这些配置更改后销毁重建你的这个 Activity
而是直接调用它的 onConfigurationChanged()
回调方法,你须要在这个回调中本身处理配置更改后的行为。Activity
的销毁重建不但发生在设备配置更改后,只要用户离开了某个 Activity
,那么那个 Activity
就随时可能被系统销毁。因此销毁重建是没法避免的,也不该该逃避,而是应该想办法保存和恢复状态<uses-feature>
标签声明只有知足这些软硬件要求的设备才能安装,经过它的 android:required
属性设置该要求是否是必须的,程序中能够经过 PackageManager.hasSystemFeature()
方法判断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#onCreate()
方法中提交事务是没问题的,由于你能够在里面根据保存的状态重建,可是在其余生命周期回调中提交事务就可能会出现问题了。FragmentActivity#onPostResume()
方法中调用了 FragmentActivity#onResumeFragments()
方法完成其关联的全部的 Fragment 的 resume 事件的分发,执行完这两个方法 Activity 和它关联的全部 Fragment 才算真正的 resumed,才算恢复了状态,才能够提交事务,因此若是非要在 Activity#onCreate()
以外的回调中提交事务那么 FragmentActivity#onPostResume()
和 FragmentActivity#onResumeFragments()
是最好的选择。避免在异步的回调中提交事务: 由于在这些回调执行的时候很难肯定当前 Activity 正处于什么生命周期状态,并且忽然地提交事务更改大量 UI 会产生糟糕的用户体验,因此若是遇到这样的场景能够考虑换一种实现思路,不要随便使用 commitAllowingStateLoss()
方法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()
方法能够移除当前任务requestPermissions()
并不意味着系统必定会弹出权限请求对话框,也就是说不能假设调用该方法后就发生了用户交互,由于若是用户以前勾选了 “禁止后再也不询问” 或者系统策略禁止应用获取权限,那么系统会直接拒绝这次权限请求,没有任何交互Fragment
中的 onRequestPermissionsResult()
方法只有在使用 Fragment#requestPermissions()
方法申请权限时才可能接收到回调,建议将权限放在所属 Activity
中申请和处理onActivityResult()
回调中接收数据就好了。可是有一点必定要注意,若是你在 AndroidManifest.xml
文件中声明了相机权限,你就必须得动态申请并得到相机权限才能拉起系统相机// 请求通信录权限的模板代码以下
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: 在用户主动授予权限后从新检查权限,但不要在这里进行事务提交等生命周期敏感操做
}
}
复制代码
android:shortcutId
和 android:shortcutShortLabel
属性是必须的,android:shortcutShortLabel
不能超过 10 个字符,android:shortcutLongLabel
不能超过 25 个字符,android:icon
不能包含 tintShortcutManager
的方式有两个: getSystemService(ShortcutManager.class)
和 getSystemService(Context.SHORTCUT_SERVICE)
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
间传递数据,保存实例状态也是用它,Bundle
是它的一个实现,最好只用它存储和传递少许数据,别超过 50k,不然既可能影响性能又可能致使崩溃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
也能够阻止系统睡眠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 能够经过覆写 View 的 fitSystemWindows(Rect insets)
方法(API level 20 以上覆写 onApplyWindowInsets(WindowInsets insets)
方法)完成View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
,隐藏状态栏和导航栏,任何交互都会清除 flag 使系统栏保持可见View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
,隐藏状态栏和导航栏,从被隐藏的系统栏边缘向内滑动会使系统栏保持可见,应用没法响应这个手势View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
,隐藏状态栏和导航栏,从被隐藏的系统栏边缘向内滑动会使系统栏暂时可见,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
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"/>
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 适配》setStatusBarColor()
方法设置状态栏背景色,要求 window 必须添加 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
的 flag 而且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
的 flagsetSystemUiVisibility()
方法设置 View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
flag 兼容亮色背景的状态栏,一样要求 window 必须添加 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
的 flag 而且清除 WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
的 flagandroid.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
接口的空实现getResources().getInteger(android.R.integer.config_shortAnimTime)
animate()
方法获取 ViewPropertyAnimator
对象,链式调用这个对象的 scaleX()
、alpha()
等方法能够简单方便地同时对 view 的多个属性作动画res/animator/
目录下,ValueAnimator
对应 <animator>
,ObjectAnimator
对应 <objectAnimator>
,AnimatorSet
对应 <set>
,使用 AnimatorInflater.loadAnimator()
能够加载这些动画AnimationDrawable
对应于 XML 文件中的 <animation-list>
,保存目录为 res/drawable/
,AnimationDrawable
的 start()
方法能够在 onStart()
中调用。还有一种是 AnimatedVectorDrawable
,须要 res/drawable/
中的 <animated-vector>
引用 res/drawable/
中的 <vector>
对其使用 res/animator/
中的 <objectAnimator>
动画GONE
→ 开始动画时将淡入 view 的 alpha 设置为 0,visibility 设置为 VISIBLE
→ 将淡入 view 的 alpha 动画到 1,将淡出 view 的 alpha 动画到 0 并在动画结束时将淡出 view 的 visibility 设置为 GONE
card_flip_right_in
、card_flip_right_out
、card_flip_left_in
、card_flip_left_out
INVISIBLE
→ 利用 ViewAnimationUtils.createCircularReveal()
方法建立半径从 0 到 Math.hypot(cx, cy)
的圆形裁剪动画 → 将 view 的 visibility 设置为 VISIBLE
而后开启动画ObjectAnimator.ofFloat()
方法动画设置 view 的 translationX
或 translationY
属性便可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()
方法判断是不是阻尼动画X
,Y
,SCALE_X
,SCALE_Y
属性便可,不过要先计算好两个 view 最终的位置和初始缩放比android:animateLayoutChanges="true"
属性告诉系统开启默认动画,或者经过 LayoutTransition
API 设置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
的大小过渡动画可能有问题android:windowActivityTransitions
属性设置为 true
或者代码中手动 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
开启。setExitTransition()
和 setSharedElementExitTransition()
方法能够为起始 Activity 设置退出过渡动画,setEnterTransition()
和 setSharedElementEnterTransition()
方法能够为目标 Activity 设置进入过渡动画。激活目标 Activity 的时候须要携带 ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
的 Bundle,返回的时候要使用 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 须要更改的元素数TextView
宽和高都不能是 wrap_content
,autoSizeTextType
默认是 none
,设置为 uniform
开启自适应,默认最小 12sp
,最大 112sp
,粒度 1px
。autoSizePresetSizes
属性能够设置预置的一些大小res/font/
,使用属性为 fontFamily
,获取 Typeface
为 getResources().getFont(R.font.myfont);
,兼容库使用 ResourcesCompat.getFont(context, R.font.myfont)
Magnifier
的 show()
方法的参数是相对于被放大 View 的左上角的坐标<com.myapp.MyDrawable>
,公共静态内部类可使用 class 属性 class="com.myapp.MyTopLevelClass$MyDrawable"
vectorDrawables.useSupportLibrary = true
并使用 VectorDrawableCompat
和 AnimatedVectorDrawableCompat
NETWORK_STATE_CHANGED_ACTION
广播再也不包含 SSID,BSSID 等信息BroadcastReceiver
,免除这项限制的广播包括 ACTION_LOCKED_BOOT_COMPLETED
等不太可能影响用户体验的广播ACTION_NEW_PICTURE
和 ACTION_NEW_VIDEO
系统广播,能够经过 JobInfo
和 JobParameters
完成。不能静态注册 CONNECTIVITY_ACTION
广播,若是想在网络变化时调度任务能够选择使用 WorkManager,若是只在应用运行期间监听网络变化使用 ConnectivityManager
比动态注册注销 BroadcastReceiver
更优雅BroadcastReceiver
onReceive()
方法中不能进行复杂工做不然会致使 ANR,onReceive()
方法一旦执行完,系统可能就认为这个广播接收器已经没用了,随时会杀掉包含这个广播接收器的进程,包括这个进程启动的线程。使用 goAsync()
方法能够在 PendingResult#finish()
方法执行前为广播接收器的存活争取更多的时间,但最好仍是使用 JobScheduler
等方式进行长时间处理工做sendBroadcast()
方法发的广播属于常规广播,全部能接收这个广播的广播接收器接收到广播的顺序是不可控的sendOrderedBroadcast()
方法发的广播属于有序广播,根据广播接收器的优先级一个接一个地传递这条广播,相同优先级的顺序不可控,广播接收器能够选择继续传递给下一个,也能够选择直接丢掉LocalBroadcastManager.getInstance(this).sendBroadcast()
方法发的广播属于应用进程内的本地广播,这样的广播只有应用本身知道,比系统级的全局广播更安全更有效率/data/data/your.application.package/
或 /data/user/0/your.application.package/
,当卸载应用时这个目录也会被删除。这个目录除了系统和应用本身谁都没法访问,除非拥有权限。这个路径下有个 files/
子目录用来存储应用的文件,能够经过 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)
表示可读。外存根目录可使用 Environment.getExternalStorageDirectory()
方法获取,通常是 /storage/emulated/0/
,使用 new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), albumName)
能够读写外存公有目录的文件。使用 getExternalFilesDir(null)
能够获取该应用的外存根目录,通常是 /storage/emulated/0/Android/data/your.application.package/files
,使用 new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), albumName)
能够读写文件,应用的外存目录也会在卸载应用时被删除。使用 getExternalCacheDir()
能够获取应用的外存缓存目录。myFile.delete()
或 myContext.deleteFile(fileName)
删除文件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()
方法会异步写到磁盘。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
属性通常是以当前应用包名为前缀的字符串,用来标志数据的全部者,多个的话用分号隔开<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 = FileProvider.getUriForFile(getContext(), "com.example.myapp.fileprovider", newFile);
复制代码
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()
方法后或者重启系统后才会撤销受权List<ResolveInfo> activities = getPackageManager().queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if (activities.size() > 0) {
for (ResolveInfo resolveInfo : activities) {
grantUriPermission(resolveInfo.activityInfo.packageName,
outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
...
revokeUriPermission(outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
复制代码
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);
复制代码
ContentProvider
所在应用自己的组件能够随便访问它,不须要受权ContentProvider
的应用不指定任何权限,那么其它应用就没法访问这个 ContentProvider
的数据<uses-permission>
标签获取访问权限ContentProvider
须要继承 ContentProvider
并实现增删改查等一系列方法: onCreate()
在系统建立 provider 后立刻调用,能够在这里建立数据库,但不要在这里作耗时操做。getType()
返回 content URI 的 MIME 类型。query()
、insert()
、update()
、delete()
进行增删改查。除了 onCreate()
方法其它方法必需要保证是线程安全的REQUEST_INSTALL_PACKAGES
权限,数据必须经过 FileProvider
形式共享,数据类型是 application/vnd.android.package-archive
,必须给 Intent 添加 FLAG_GRANT_READ_URI_PERMISSION
flag 授予临时访问权限Intent installIntent = new Intent(Intent.ACTION_VIEW);
File apkPath = new File(Environment.getExternalStorageDirectory(), "apks");
File apkFile = new File(apkPath, "myapp.apk");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri contentUri = FileProvider.getUriForFile(MainActivity.this, "com.example.myapp.fileprovider", apkFile);
installIntent.setDataAndType(contentUri, "application/vnd.android.package-archive");
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
installIntent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
}
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (installIntent.resolveActivity(getPackageManager()) != null) {
startActivity(installIntent);
}
复制代码
IMPORTANCE_HIGH
对应 PRIORITY_HIGH
或 PRIORITY_MAX
,IMPORTANCE_DEFAULT
对应 PRIORITY_DEFAULT
,IMPORTANCE_LOW
对应 PRIORITY_LOW
,IMPORTANCE_MIN
对应 PRIORITY_MIN
。在应用启动时能够执行下面的代码建立通知类别,能够无反作用地屡次执行private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name);
String description = getString(R.string.channel_description);
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
复制代码
NotificationChannel
的 enableLights()
,setLightColor()
等方法能够指定该通知类别默认的通知行为,可是一旦建立了应用就不能再对它作任何更改了,只有用户本身能够更改设置。能够经过 Intent 引导用户跳转至对应设置页Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, myNotificationChannel.getId());
startActivity(intent);
复制代码
getNotificationChannel()
、getNotificationChannels()
、getVibrationPattern()
、getImportance()
等方法获取deleteNotificationChannel(id)
能够删除通知类别,可是在开发模式下可能须要重装应用或者清除数据才会彻底删除// The id of the group.
String groupId = "my_group_01";
// The user-visible name of the group.
CharSequence groupName = getString(R.string.group_name);
NotificationManager mNotificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannelGroup(new NotificationChannelGroup(groupId, groupName));
复制代码
setCategory()
方法能够设置所属的系统范围的勿扰类别NotificationCompat
和 NotificationManagerCompat
等兼容库中的类以便方便地适配低版本系统setSmallIcon()
方法能够设置小图标,应用名和时间是由系统设置的,setLargeIcon()
方法能够设置右边大图标,setContentTitle()
和 setContentText()
方法能够设置通知的标题和内容,setPriority()
方法能够为 Android 8.0 (API level 26) 如下的系统设置通知优先级。系统范围的预约义通知类别包括 NotificationCompat.CATEGORY_ALARM
,NotificationCompat.CATEGORY_REMINDER
等类别,这个类别在勿扰模式中有用,能够经过 setCategory()
方法指定所属的系统范围通知类别setStyle()
方法设置其余可展开的通知样式,.setStyle(new NotificationCompat.BigTextStyle().bigText(emailObject.getSubjectAndSnippet()))
能够设置大文本块样式。.setStyle(new NotificationCompat.InboxStyle().addLine(messageSnippet1)
能够设置多行的 inbox 样式。.setStyle(new NotificationCompat.MessagingStyle(resources.getString(R.string.reply_name)).addMessage(message1))
能够设置消息样式,可是此样式会忽略 setContentTitle()
和 setContentText()
方法的设置,但能够经过 setConversationTitle()
方法设置该聊天所属的群组名。setStyle(new android.support.v4.media.app.Notification.MediaStyle().setShowActionsInCompactView(1).setMediaSession(mMediaSession.getSessionToken()))
能够设置媒体样式的通知,属于 CATEGORY_TRANSPORT
类别。setContentIntent()
方法设置 PendingIntent
对象完成,setAutoCancel(true)
能够在点击后自动移除通知Intent intent = new Intent(this, AlertDetails.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setLargeIcon(myBitmap)
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(myBitmap)
.bigLargeIcon(null))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
复制代码
NotificationManagerCompat#notify()
方法能够显示通知,你须要定义一个惟一的 int 值的 ID 做为这个通知的 ID,保存这个 ID 以便以后更新或移除这个通知NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, mBuilder.build());
复制代码
addAction()
方法能够添加操做按钮private static final String KEY_TEXT_REPLY = "key_text_reply";
String replyLabel = getResources().getString(R.string.reply_label);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel(replyLabel)
.build();
PendingIntent replyPendingIntent =
PendingIntent.getBroadcast(getApplicationContext(),
conversation.getConversationId(),
getMessageReplyIntent(conversation.getConversationId()),
PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action action =
new NotificationCompat.Action.Builder(R.drawable.ic_reply_icon,
getString(R.string.label), replyPendingIntent)
.addRemoteInput(remoteInput)
.build();
Notification newMessageNotification = new Notification.Builder(mContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_message)
.setContentTitle(getString(R.string.title))
.setContentText(getString(R.string.content))
.addAction(action)
.build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, newMessageNotification);
复制代码
private CharSequence getMessageText(Intent intent) {
// 在 BroadcastReceiver 接收的 Intent 中能够根据以前的 KEY 拿到文本框的内容
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(KEY_TEXT_REPLY);
}
return null;
}
复制代码
// 在回复完成后更新通知表示已经处理此次回复,也能够调用 setRemoteInputHistory() 方法附加回复的内容
Notification repliedNotification = new Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_message)
.setContentText(getString(R.string.replied))
.build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(notificationId, repliedNotification);
复制代码
setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false)
能够给通知添加肯定进度条,经过 setProgress(0, 0, true)
能够添加不肯定进度条,经过 setProgress(0, 0, false)
能够在完成后移除进度条setVisibility()
方法能够设置锁屏时的通知显示策略,VISIBILITY_PUBLIC
(显示全部通知)表示完整地显示通知内容,VISIBILITY_SECRET
(彻底不显示内容)表示不显示通知的任何信息,VISIBILITY_PRIVATE
(隐藏敏感通知内容)表示只显示图标标题等基本信息NotificationManagerCompat#notify()
方法传递以前的通知 ID 能够更新以前的通知,调用 setOnlyAlertOnce()
方法以便只在第一次出现通知时提示用户setAutoCancel()
方法能够在用户点击通知后清除通知,建立通知时调用 setTimeoutAfter()
方法能够在超时后由系统自动清除通知,能够随时调用 cancel()
或 cancelAll()
方法清除以前的通知Activity
分为两种,一种是应用的正经常使用户体验流中的常规 Activity
,拥有任务完整的返回栈。还有一种是仅仅用来展现通知的详细内容的特殊Activity
,它不须要返回栈。Activity
须要先经过 android:parentActivityName
属性或者 android.support.PARENT_ACTIVITY
的 <meta-data>
标签指定层级关系,而后Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
复制代码
Activity
须要先指定 android:taskAffinity=""
和 android:excludeFromRecents="true"
以免在以前的任务中启动,而后Intent notifyIntent = new Intent(this, ResultActivity.class);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(
this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
);
复制代码
setGroup()
方法指定所属分组,经过 setSortKey()
方法排序,经过 setGroupAlertBehavior()
指定通知行为,默认是 GROUP_ALERT_ALL
表示组内全部的通知均可能产生声音和震动。系统默认会自动生成通知组的摘要,你也能够单首创建一个表示通知组摘要的通知int SUMMARY_ID = 0;
String GROUP_KEY_WORK_EMAIL = "com.android.example.WORK_EMAIL";
Notification newMessageNotification1 =
new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify_email_status)
.setContentTitle(emailObject1.getSummary())
.setContentText("You will not believe...")
.setGroup(GROUP_KEY_WORK_EMAIL)
.build();
Notification newMessageNotification2 =
new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify_email_status)
.setContentTitle(emailObject2.getSummary())
.setContentText("Please join us to celebrate the...")
.setGroup(GROUP_KEY_WORK_EMAIL)
.build();
Notification summaryNotification =
new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID)
.setContentTitle(emailObject.getSummary())
//set content text to support devices running API level < 24
.setContentText("Two new messages")
.setSmallIcon(R.drawable.ic_notify_summary_status)
//build summary info into InboxStyle template
.setStyle(new NotificationCompat.InboxStyle()
.addLine("Alex Faarborg Check this out")
.addLine("Jeff Chang Launch Party")
.setBigContentTitle("2 new messages")
.setSummaryText("janedoe@example.com"))
//specify which group this notification belongs to
.setGroup(GROUP_KEY_WORK_EMAIL)
//set this notification as the summary for the group
.setGroupSummary(true)
.build();
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(emailNotificationId1, newMessageNotification1);
notificationManager.notify(emailNotificationId2, newMessageNotification2);
notificationManager.notify(SUMMARY_ID, summaryNotification);
复制代码
mChannel.setShowBadge(false)
能够禁用小圆点标志,调用 setNumber(messageCount)
能够设置长按后显示给用户的消息数,调用 setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
能够设置长按后的图标样式,经过 setShortcutId()
能够隐藏重复的 shortcutsetStyle(new NotificationCompat.DecoratedCustomViewStyle())
样式,而后调用 setCustomContentView()
和 setCustomBigContentView()
方法指定自定义的折叠和展开布局(通常折叠布局限制高度为 64 dp,展开布局高度限制为 256 dp),布局中的控件要使用兼容库的样式,如 style="@style/TextAppearance.Compat.Notification.Title"
。若是不想使用标准通知模板,不调用 setStyle()
只调用 setCustomBigContentView()
便可Activity
正处于全屏模式并使用了 fullScreenIntent
;Android 8.0 (API level 26) 及更高的设备上通知的重要程度为 IMPORTANCE_HIGH
;Android 8.0 (API level 26) 如下的设备上通知的优先级为 PRIORITY_HIGH
或 PRIORITY_MAX
而且开启了铃声或震动Service
运行在所在进程的主线程中,它不会自动建立线程,若是你不指定进程的话它甚至不会建立额外的进程,因此若是要执行耗时操做的话应该建立一个新的线程去执行Service
有三种类型,一种是 Foreground service,用来执行用户能从通知栏感知到的后台操做,一种是 Background service,是用户感知不到的,可是 Android 8.0 (API level 26) 开始系统会对这类 Service
进行各类限制,一种是 Bound service,是经过 bindService()
方法绑定到其余组件的服务,Bound service 基于 C/S 架构的思想,被绑定的组件能够向这个服务发请求、接收响应数据、甚至 IPC 交互,只要有一个组件绑定了它,他就会立刻运行,多个组件能够同时绑定它,当全部都解绑时,它就会被销毁startService()
方法启动 Service
时会致使它收到 onStartCommand()
回调,服务会一直运行直到你主动调用它的 stopSelf()
方法或其它组件主动调用 stopService()
方法。若是它只是 Bound service 能够不是实现该方法bindService()
方法启动 Service
时不会回调 onStartCommand()
方法,而会回调 onBind()
方法,为了与它的 client 通讯这个方法须要返回 IBinder
,若是你不容许它绑定就返回空,解绑使用 unbindService()
方法Service
能够启动屡次,可是只能中止一次,stopSelf(int)
方法能够在知足指定的 startId
时中止Service
和 Activity
同样其所在的的进程可能随时被系统杀掉,一样须要作好销毁重建的工做IntentService
做为 Service
的子类能够方便地在工做线程中完成多个任务,多个任务是一个接一个的执行,因此不会存在线程安全问题,内部是借助一个 HandlerThread
实现异步处理的,当全部请求都完成后会自动销毁,onBind()
方法返回了空,为了方便调试能够在构造器中指定工做线程的名字,若是想重写 onCreate()
、onStartCommand()
、onDestroy()
方法的实现必须调用父类的实现,IntentService
是在一个工做线程中完成多个任务,因此若是想在多个线程中完成多个任务能够直接继承 Service
并借助 HandlerThread
等线程技术实现IntentService
更推荐使用 JobIntentService
,JobIntentService
借助了 JobScheduler
和 AsyncTask
完成更灵活的任务调度和处理,只须要申请好 WAKE_LOCK
权限 JobScheduler
就能够完成 WakeLock
的管理,使用 enqueueWork(Context, Class, int, Intent)
静态方法提交任务就可让 onHandleWork(Intent)
回调中的代码被更好地调度执行了Service
只能经过 PendingIntent
进行组件间的通讯FOREGROUND_SERVICE
权限startForeground()
方法能够向系统请求以 Foreground service 模式运行,stopForeground()
能够请求退出该模式JobScheduler
实现,在以前的系统上借助 BroadcastReceiver
和 AlarmManager
实现OneTimeWorkRequest
,周期性的任务使用 PeriodicTimeWorkRequest
Constraints constraints = new Constraints.Builder()
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.build();
OneTimeWorkRequest compressionWork =
new OneTimeWorkRequest.Builder(CompressWorker.class)
.setConstraints(constraints)
.build();
复制代码
setInitialDelay(10, TimeUnit.MINUTES)
设置一个最小延时Worker
中使用 Result.retry()
完成,采用的补偿策略默认是 EXPONENTIAL
指数级的,可使用 setBackoffCriteria()
方法调整策略setInputData()
方法为任务设置输入数据,在 Worker
中能够经过 getInputData()
方法获取到输入数据,Result.success()
和 Result.failure()
能够携带输出数据。数据应该尽量的简单,不能超过 10KBaddTag
方法能够给任务打 Tag,而后就可使用 WorkManager.cancelAllWorkByTag(String)
和 WorkManager.getWorkInfosByTagLiveData(String)
等方法方便操做任务了BLOCKED
态ENQUEUED
态RUNNING
态Result.success()
那么会被认为是 SUCCEEDED
态,这是最终态,只有 OneTimeWorkRequest
可能进入这个状态Result.failure()
那么会被认为是 FAILED
态,这是最终态,只有 OneTimeWorkRequest
可能进入这个状态,全部相关的任务也会被标记为 FAILED
且不会被执行WorkRequest
会被认为是 CANCELLED
态,全部相关的任务也会被标记为 CANCELLED
且不会被执行WorkManager.getWorkInfoById(UUID)
和 WorkManager.getWorkInfoByIdLiveData(UUID)
等方法能够定位想要的任务进行观察WorkManager.getInstance()
.beginWith(Arrays.asList(filter1, filter2, filter3))
.then(compress)
.then(upload)
.enqueue();
复制代码
对于用户触发的必须立刻执行且必须执行完的后台任务,须要使用 Foreground services 实现,它既告诉系统这个应用正在执行重要的任务不能被杀掉,又经过通知栏告诉用户有后台工做正在执行php
若是任务须要在精确的时间点执行,可使用 AlarmManager
java
若是须要执行一个长时间的 HTTP 下载任务,可使用 DownloadManager
。DownloadManager
独立于应用以外,能够在下载失败、更改网络链接、系统重启后进行重试android
public static long downloadApk(String url, String title, String desc) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
.setTitle(title)
.setDescription(desc)
.setDestinationInExternalFilesDir(MyApplication.getInstance(), null, "apks")
.allowScanningByMediaScanner();
DownloadManager downloadManager = (DownloadManager) MyApplication.getInstance().getSystemService(Context.DOWNLOAD_SERVICE);
return downloadManager.enqueue(request);
}
复制代码
adb shell am start
-W -a android.intent.action.VIEW
-d "example://gizmos" com.example.android
复制代码
adb shell am start -a android.intent.action.VIEW
-c android.intent.category.BROWSABLE
-d "http://domain.name:optional_port"
复制代码
adb shell dumpsys package domain-preferred-apps
复制代码
adb shell am kill com.some.package
复制代码
adb push com.some.package /sdcard/
复制代码
.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;
}
}
复制代码
findViewById(R.id.chooseImg).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_IMAGE_GET);
}
}
});
findViewById(R.id.takePicture).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {
ex.printStackTrace();
}
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(MainActivity.this,
"com.your.package.fileprovider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
}
}
}
});
...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_GET && resultCode == RESULT_OK) {
Uri fullPhotoUri = data.getData();
ParcelFileDescriptor descriptor;
try {
descriptor = getContentResolver().openFileDescriptor(fullPhotoUri, "r");
FileDescriptor fd = descriptor.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
imageView.setImageBitmap(bitmap);
processImage();
} catch (Exception e) {
e.printStackTrace();
}
} else if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
int targetW = imageView.getWidth();
int targetH = imageView.getHeight();
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
Bitmap bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
imageView.setImageBitmap(bitmap);
processImage();
}
}
private File createImageFile() throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(imageFileName, ".jpg", storageDir);
currentPhotoPath = image.getAbsolutePath();
return image;
}
复制代码
<paths>
<external-path name="my_images" path="Android/data/com.your.package/files/Pictures" />
</paths>
复制代码