Android 10 适配攻略,最新阿里 Android 面试题目
try {
File file = new File(imgFile.getAbsolutePath() + File.separator +
System.currentTimeMillis() + ".jpg");
// 使用 openInputStream(uri)方法获取字节输入流
InputStream fileInputStream = getContentResolver().openInputStream(uri);
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int byteRead;
while (-1 != (byteRead = fileInputStream.read(buffer))) {
fileOutputStream.write(buffer, 0, byteRead);
}
fileInputStream.close();
fileOutputStream.flush();
fileOutputStream.close();
// 文件可用新路径 file.getAbsolutePath()
} catch (Exception e) {
e.printStackTrace();
}
如果你要获取图片中的地理位置信息,需要申请 ACCESS_MEDIA_LOCATION 权限,并使用 MediaStore.setRequireOriginal()获取。下面是官方的示例代码:
Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex));
final double[] latLong;
// 从 ExifInterface 类获取位置信息
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
ExifInterface exifInterface = new ExifInterface(stream);
double[] returnedLatLong = exifInterface.getLatLong();
// If lat/long is null, fall back to the coordinates (0, 0).
latLong = returnedLatLong != null ? returnedLatLong : new double[2];
// Don't reuse the stream associated with the instance of "ExifInterface".
stream.close();
} else {
// Failed to load the stream, so return the coordinates (0, 0).
latLong = new double[2];
}
这样下来,一个图片选择器就基本适配完了。
[](
)补充
应用在卸载后,会将 App-specific 目录下的数据删除,如果在 AndroidManifest.xml 中声明:android:hasFragileUserData="true"用户可以选择是否保留。
对于 SAF 的使用,可以查看我之前写的 SAF 使用攻略,这里就不展开说了。
最后这里有一个介绍 Scoped Storage 的视频,推荐观看:
准备好使用分区存储 | ADS 中文字幕视频
准备好使用分区存储
[](
)2.权限变化
=================================================================
从 6.0 开始,基本每次都会有权限方面变动,这次也不例外。(前几天发布了 Android 11 的预览版,看来也有权限方面的变化。。。单次权限即将到来)
[](
)1.在后台运行时访问设备位置信息需要权限
Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限(危险权限)。
<uses-permission android:name="android.per
mission.ACCESS_BACKGROUND_LOCATION"/>
该权限允许应用程序在后台访问位置。如果请求此权限,则还必须请求 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限。只请求此权限无效果。
在 Android 10 的设备上,如果你的应用的 targetSdkVersion < 29,则在请求 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限时,系统会自动同时请求 ACCESS_BACKGROUND_LOCATION。在请求弹框中,选择“始终允许”表示同意后台获取位置信息,选择“仅在应用使用过程中允许”或"拒绝"选项表示拒绝授权。
如果你的应用的 targetSdkVersion >= 29,则请求 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限表示在前台时拥有访问设备位置信息的权。在请求弹框中,选择“始终允许”表示前后台都可以获取位置信息,选择“仅在应用使用过程中允许”只表示拥有前台的权限。
总结一下就是下图:
其实官方不推荐你使用申请后台访问权的方式,因为这样的结果无非就是多请求一个权限,那么这像变更还有什么意义?申请过多的权限,也会造成用户的反感。所以官方推荐使用前台服务来实现,在前台服务中获取位置信息。
首先在清单中对应的 service 中添加 android:foregroundServiceType=“location”:
<service
android:name="MyNavigationService"
android:foregroundServiceType="location" ... >
...
</service>
启动前台服务前检查是否具有前台的访问权限:
boolean permissionApproved = ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
if (permissionApproved) {
// 启动前台服务
} else {
// 请求前台访问位置权限
}
如此一来就可以在 Service 中获取位置信息。
[](
)2.一些电话、蓝牙和 WLAN 的 API 需要精确位置权限
下面列举了 Android 10 中必须具有 ACCESS_FINE_LOCATION 权限才能使用类和方法:
电话
TelephonyManager
getCellLocation()
getAllCellInfo()
requestNetworkScan()
requestCellInfoUpdate()
getAvailableNetworks()
getServiceState()
TelephonyScanManager
requestNetworkScan()
TelephonyScanManager.NetworkScanCallback
onResults()
PhoneStateListener
onCellLocationChanged()
onCellInfoChanged()
onServiceStateChanged()
WLAN
WifiManager
startScan()
getScanResults()
getConnectionInfo()
getConfiguredNetworks()
WifiAwareManager
WifiP2pManager
WifiRttManager
蓝牙
BluetoothAdapter
startDiscovery()
startLeScan()
BluetoothAdapter.LeScanCallback
BluetoothLeScanner
startScan()
我们可以根据上面提供的具体类和方法,在适配项目中检查是否有使用到并及时处理。
[](
)3.ACCESS_MEDIA_LOCATION
Android 10 新增权限,上面有提到,不赘述了。
[](
)4.PROCESS_OUTGOING_CALLS
Android 10 上该权限已废弃。
[](
)3.后台启动 Activity 的限制
==============================================================================
简单解释就是应用处于后台时,无法启动 Activity。比如点开一个应用会进入启动页或者广告页,一般会有几秒的延时再跳转至首页。如果这期间你退到后台,那么你将无法看到跳转过程。而在之前的版本中,会强制弹出页面至前台。
既然是限制,那么肯定有不受限的情况,主要有以下几点:
应用具有可见窗口,例如前台 Activity。
应用在前台任务的返回栈中已有的 Activity。
应用在 Recents 上现有任务的返回栈中已有的 Activity。Recents 就是我们的任务管理列表。
应用收到系统的 PendingIntent 通知。
应用收到它应该在其中启动界面的系统广播。示例包括 ACTION_NEW_OUTGOING_CALL 和 SECRET_CODE_ACTION。应用可在广播发送几秒钟后启动 Activity。
用户已向应用授予 SYSTEM_ALERT_WINDOW 权限,或是在应用权限页开启后台弹出页面的开关。
因为此项行为变更适用于在 Android 10 上运行的所有应用,所以这一限制导致最明显的问题就是点击推送信息时,有些应用无法进行正常的跳转(具体的实现问题导致)。所以针对这类问题,可以采取 PendingIntent 的方式,发送通知时使用 setContentIntent 方法。
当然你也可以申请相应权限或者白名单:
不过申请白名单这种方法受各种手机厂商所限,很麻烦。感觉还不如引导用户手动开启权限。。。
对于全屏 intent,注意设置最高优先级和添加 USE_FULL_SCREEN_INTENT 权限,这是一个普通权限。比如微信来语音或者视频通话时,弹出的接听页面就是使用这一功能。
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
Intent fullScreenIntent = new Intent(this, CallActivity.class);
PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("Incoming call")
.setContentText("(919) 555-1234")
.setPriority(NotificationCompat.PRIORITY_HIGH) // <--- 高优先级
.setCategory(NotificationCompat.CATEGORY_CALL)
// Use a full-screen intent only for the highest-priority alerts where you
// have an associated activity that you would like to launch after the user
// interacts with the notification. Also, if your app targets Android 10
// or higher, you need to request the USE_FULL_SCREEN_INTENT permission in
// order for the platform to invoke this notification.
.setFullScreenIntent(fullScreenPendingIntent, true); // <--- 全屏 intent
Notification incomingCallNotification = notificationBuilder.build();
注意:在部分手机上,直接设置 setPriority 无效(或者说以渠道优先级为准)。所以需要创建通知渠道时将重要性设置为 IMPORTANCE_HIGH。
NotificationChannel channel = new NotificationChannel(channelId, "xxx", NotificationManager.IMPORTANCE_HIGH);
后台启动 Activity 的限制的目的是为了减少对用户操作的中断。如果你有要弹出的页面,推荐你先弹出通知,让用户自己选择接下来的操作,而不是一股脑的强制弹出。(如果你的全屏 intent 都让用户反感,那他也可以关掉你的通知,不至于任你摆布。)
[](
)4.深色主题
=================================================================
Android 10 新增了一个系统级的深色主题(在系统设置中开启)。虽然深色主题并不是强制适配项,但是它可以带给用户更好的体验:
可大幅减少耗电量。 OLED 屏幕中每个像素都是自主发光,所以在显示深色元素时像素所消耗的电流更低,尤其在纯黑颜色时像素点可以完全关闭来达到省电的效果。
为弱视以及对强光敏感的用户提高可视性。深色可以降低屏幕的整体视觉亮度,减少对眼睛的视觉压力。
让所有人都可以在光线较暗的环境中更轻松地使用设备。
适配方法有两种:
[](
)1.手动适配(资源替换)
官方文档中提到的继承 Theme.AppCompat.DayNight 或者 Theme.MaterialComponents.DayNight 的方法,但这只是将我们使用的各种 View 的默认样式进行了适配,并不太适用于实际项目的适配。因为具体的项目中的 View 都按照设计的风格进行了重定义。
其实适配的方法很简单,类似屏幕适配、国际化的操作,并不需要继承上面的主题。比如你要修改颜色,就在 res 下新建 values-night 目录,创建对应的 colors.xml 文件。将具体要修改的色值定义在里面。图标之类的也是一个思路,创建对应的 drawable-night 目录。
只要你之前的代码不是硬编码且代码规范,那么适配起来还是很轻松。
[](
)2.自动适配(Force Dark)
Android 10 提供 Force Dark 功能。一如其名,此功能可让开发者快速实现深色主题背景,而无需明确设置 DayNight 主题背景。
如果您的应用采用浅色主题背景,则 Force Dark 会分析应用的每个视图,并在相应视图在屏幕上显示之前,自动应用深色主题背景。有些开发者会混合使用 Force Dark 和本机实现,以缩短实现深色主题背景所需的时间。
应用必须选择启用 Force Dark,方法是在其主题背景中设置 android:forceDarkAllowed=“true”。此属性会在所有系统及 AndroidX 提供的浅色主题背景(例如 Theme.Material.Light)上设置。使用 Force Dark 时,您应确保全面测试应用,并根据需要排除视图。
如果您的应用使用 Dark Theme 主题(例如 Theme.Material),则系统不会应用 Force Dark。同样,如果应用的主题背景继承自 DayNight 主题(例如 Theme.AppCompat.DayNight),则系统不会应用 Force Dark,因为会自动切换主题背景。
您可以通过 android:forceDarkAllowed 布局属性或 setForceDarkAllowed(boolean) 在特定视图上控制 Force Dark。
上述内容我直接照搬文档的说明。总结一下,使用 Force Dark 需要注意几点:
如果使用的是 DayNight 或 Dark Theme 主题,则设置 forceDarkAllowed 不生效。
如果有需要排除适配的部分,可以在对应的 View 上设置 forceDarkAllowed 为 false。
这里说说我实际使用此方法的感受:整体还是不错的,设置的色值会自动取反。但也因此颜色不受控制,能否达到预期效果是个需要注意的问题。追求快速适配可以采取此方案。
[](
)手动切换主题
使用 AppCompatDelegate.setDefaultNightMode(@NightMode int mode)方法,其中参数 mode 有以下几种:
浅色 - MODE_NIGHT_NO
深色 - MODE_NIGHT_YES
由省电模式设置 - MODE_NIGHT_AUTO_BATTERY
系统默认 - MODE_NIGHT_FOLLOW_SYSTEM
下面的代码是官方 Demo 中的使用示例:
public class ThemeHelper {
public static final String LIGHT_MODE = "light";
public static final String DARK_MODE = "dark";
public static final String DEFAULT_MODE = "default";
public static void applyTheme(@NonNull String themePref) {
switch (themePref) {
case LIGHT_MODE: {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
}
case DARK_MODE: {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
}
default: {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
}
break;
}
}
}
}
通过 AppCompatDelegate.getDefaultNightMode()方法,可以获取到当前的模式,这样便于代码中去适配。
[](
)监听深色主题是否开启
首先在清单文件中给对应的 Activity 配置 android:configChanges=“uiMode”:
评论