写点什么

Android 10 适配攻略,最新阿里 Android 面试题目

用户头像
Android架构
关注
发布于: 10 小时前

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


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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”:

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android 10 适配攻略,最新阿里Android面试题目