Android 13最近发布了,除了适配问题之外,也新增了一些功能。
本篇文章分享一下PhotoPicker和BackPress这两个新的API。
PhotoPicker
在Android 13的首个预览版刚出时,我就对新的照片选择器(PhotoPicker)很感兴趣,正式版出了之后也是第一时间试了一下。
PhotoPicker 支持单选和多选,打开的选择器默认为同时选择照片和视频,也可以通过设置更改为单独选择照片或视频。需要注意的是,MediaStore.ACTION_PICK_IMAGES
仅在Android 13(33)以上有用。
- 单选
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
//不设置时默认选择图片和视频。
//设置"image/*" 单独选择图片。
//设置"video/*" 单独选择视频。
intent.type = "image/*"
startActivityForResult(intent,PHOTO_PICKER_SINGLE_REQUEST_CODE)
- 多选
可以通过设置Intent
的Extra
来限制选择数量的上限。
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
intent.type = "image/*"
//数量上限的平台限制,也可以自定选择数,小于此限制即可。
val maxLimit = MediaStore.getPickImagesMaxLimit()
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX,maxLimit)
startActivityForResult(intent,PHOTO_PICKER_MULTI_REQUEST_CODE)
- 处理选择结果
class ExampleActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK){
when(requestCode){
PHOTO_PICKER_SINGLE_REQUEST_CODE->{
val mediumUri: Uri = data.data
}
PHOTO_PICKER_MULTI_REQUEST_CODE->{
val uriList = ArrayList<Uri>()
intent.clipData?.let {
for (index in 0 until it.itemCount) {
uriList.add(it.getItemAt(index).uri)
}
}
}
}
}
}
}
onActivityResult
已经是废弃的API,我这边结合了ActivityResult API做了个示例,代码如下:
//单选合约
class PickSingleMediumContract : ActivityResultContract<String?, Uri?>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) MediaStore.ACTION_PICK_IMAGES else Intent.ACTION_PICK)
//边配置为默认选择图片
.setType(if (input.isNullOrEmpty() || input.isNullOrEmpty()) MimeType.IMAGE_ALL else input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return if (intent == null || resultCode != Activity.RESULT_OK) null else intent.data
}
}
//多选合约
class PickMultipleMediumContract : ActivityResultContract<MultipleLauncherOptions?, List<Uri>>() {
override fun createIntent(context: Context, input: MultipleLauncherOptions?): Intent {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val maxLimit = MediaStore.getPickImagesMaxLimit()
val inputMaxCount = input?.maxCount ?: 0
val finalMaxCount = if (inputMaxCount != 0 && inputMaxCount < maxLimit) inputMaxCount else maxLimit
Intent(MediaStore.ACTION_PICK_IMAGES)
//边配置为默认选择图片
.setType(if (input?.mimeType.isNullOrEmpty() || input?.mimeType.isNullOrBlank()) MimeType.IMAGE_ALL else input?.mimeType)
.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, finalMaxCount)
} else {
Intent(Intent.ACTION_PICK)
//边配置为默认选择图片
.setType(if (input?.mimeType.isNullOrEmpty() || input?.mimeType.isNullOrBlank()) MimeType.IMAGE_ALL else input?.mimeType)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
}
override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
return if (intent == null || resultCode != Activity.RESULT_OK) {
emptyList()
} else {
getClipDataUris(intent)
}
}
private fun getClipDataUris(intent: Intent): List<Uri> {
val resultSet = LinkedHashSet<Uri>()
intent.data?.let { resultSet.add(it) }
val clipData = intent.clipData
if (clipData == null && resultSet.isEmpty()) {
return emptyList()
} else if (clipData != null) {
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
if (uri != null) {
resultSet.add(uri)
}
}
}
return ArrayList(resultSet)
}
}
//使用示例(这边以分享为例)
class SystemShareActivity : AppCompatActivity() {
private val singleMediumPicker = registerForActivityResult(PickSingleMediumContract()) { uri ->
if (uri != null) {
val pictureShareIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uri)
type = getMimeType(uri)
}
val shareIntent = Intent.createChooser(pictureShareIntent, "ShareSingleMedium")
forActivityResultLauncher.launch(shareIntent)
}
}
private val multipleMediumPicker = registerForActivityResult(PickMultipleMediumContract()) { uriList ->
if (uriList.isNotEmpty()) {
val mediumUris = ArrayList<Uri>(uriList)
var mimeType = ""
for (uri in mediumUris) {
if (mimeType.isEmpty()) {
mimeType = handleMultiplePickMimeType(getMimeType(uri))
} else {
if (mimeType != handleMultiplePickMimeType(getMimeType(uri))) {
mimeType = MimeType.ALL
break
}
}
}
val mediumShareIntent = Intent().apply {
action = Intent.ACTION_SEND_MULTIPLE
putParcelableArrayListExtra(Intent.EXTRA_STREAM, mediumUris)
type = mimeType
}
val shareIntent = Intent.createChooser(mediumShareIntent, "ShareMultipleMedium")
forActivityResultLauncher.launch(shareIntent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<LayoutSystemShareActivityBinding>(this, R.layout.layout_system_share_activity)
binding.btnSharePicture.setOnClickListener {
singleMediumPicker.launch(null)
}
binding.btnSharePictures.setOnClickListener {
multipleMediumPicker.launch(MultipleLauncherOptions(null, 5))
}
binding.btnShareVideo.setOnClickListener {
singleMediumPicker.launch(MimeType.VIDEO_All)
}
binding.btnShareVideos.setOnClickListener {
multipleMediumPicker.launch(MultipleLauncherOptions(MimeType.VIDEO_All, 5))
}
binding.btnShareMultipleMedium.setOnClickListener {
multipleMediumPicker.launch(MultipleLauncherOptions(MimeType.ALL, 5))
}
}
//获取Uri的MimeType
private fun getMimeType(uri: Uri): String {
return contentResolver.getType(uri) ?: ""
}
private fun handleMultiplePickMimeType(uriMimeType: String): String {
return when {
uriMimeType.startsWith(MimeType.IMAGE_HEAD) -> MimeType.IMAGE_ALL
uriMimeType.startsWith(MimeType.VIDEO_HEAD) -> MimeType.VIDEO_All
else -> uriMimeType
}
}
}
效果如图:
BackPress
将compileSdk和targetSdk升级到33之后,项目中的onBackPress
方法就提示废弃了,如图:
替代方案为OnBackPressedDispatcher
API。
FragmentActivity
和AppCompatActivity
的基类ComponentActivity
提供了OnBackPressedDispatcher
,用于分发返回事件。如果需要处理返回事件,可以通过onBackPressedDispatcher.addCallback
来注册回调OnBackPressedCallback
。
OnBackPressedCallback
可以通过setEnable
来设置回调的可用性,当设置为false时,即使注册了该回调,也不会执行。
通过onBackPressedDispatcher.addCallback
添加的回调,调用顺序与添加顺序相反,即后添加的先执行,并且只会执行一个回调,源码如图:
@MainThread
public void onBackPressed() {
Iterator<OnBackPressedCallback> iterator =
mOnBackPressedCallbacks.descendingIterator();
while (iterator.hasNext()) {
OnBackPressedCallback callback = iterator.next();
if (callback.isEnabled()) {
callback.handleOnBackPressed();
return;
}
}
if (mFallbackOnBackPressed != null) {
mFallbackOnBackPressed.run();
}
}
OnBackPressedCallback
的添加基于LifecycleOwner
,在Lifecycle.State.STARTED
之后添加,LifecycleOwner
销毁时自动移除,当然也可以通过onBackPressedCallback.remove()
来手动移除。
这边做了个简单的示例,代码如下:
class FragmentA : Fragment() {
lateinit var binding: LayoutBackPressApiFragmentBinding
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
isEnabled = false
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.layout_back_press_api_fragment, container, false)
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
binding.tvTitle.text = "BackPressFragmentA"
binding.btnBackPress.setOnClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
override fun onResume() {
super.onResume()
onBackPressedCallback.isEnabled = true
}
override fun onPause() {
super.onPause()
onBackPressedCallback.isEnabled = false
}
}
class FragmentB : Fragment() {
lateinit var binding: LayoutBackPressApiFragmentBinding
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
parentFragmentManager.setFragmentResult(BackPressApiActivity::class.java.canonicalName!!, Bundle().apply { putInt("result", 0) })
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.layout_back_press_api_fragment, container, false)
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
binding.tvTitle.text = "BackPressFragmentB"
binding.btnBackPress.setOnClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
override fun onResume() {
super.onResume()
onBackPressedCallback.isEnabled = true
}
override fun onPause() {
super.onPause()
onBackPressedCallback.isEnabled = false
}
}
class BackPressApiActivity : AppCompatActivity() {
private val canonicalName = this::class.java.canonicalName!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<LayoutBackPressApiActivityBinding>(this, R.layout.layout_back_press_api_activity)
supportFragmentManager.setFragmentResultListener(canonicalName, this) { requestKey, result ->
if (requestKey == canonicalName) {
val resultIndex = result.getInt("result", -1)
if (resultIndex != -1) {
binding.vpContainer.currentItem = resultIndex
}
}
}
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
finish()
}
})
binding.run {
btnAFragment.setOnClickListener {
vpContainer.currentItem = 0
}
btnBFragment.setOnClickListener {
vpContainer.currentItem = 1
}
val fragments = ArrayList<Class<out Fragment?>>()
fragments.add(FragmentA::class.java)
fragments.add(FragmentB::class.java)
vpContainer.adapter = ViewPager2Adapter(this@BackPressApiActivity, fragments)
vpContainer.isUserInputEnabled = false
vpContainer.currentItem = 0
}
}
override fun onDestroy() {
super.onDestroy()
supportFragmentManager.clearFragmentResultListener(canonicalName)
}
}
效果如图: