Android PhotoPicker和BackPress

391 阅读3分钟

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)
  • 多选

可以通过设置IntentExtra来限制选择数量的上限。

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
        }
    }
}

效果如图:

20220828_015708.gif

BackPress

将compileSdk和targetSdk升级到33之后,项目中的onBackPress方法就提示废弃了,如图:

企业微信截图_16616530851687.png

替代方案为OnBackPressedDispatcherAPI。

FragmentActivityAppCompatActivity的基类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)
    }
}

效果如图:

20220828_032259.gif