Jetpack中可能被你忽视的—行为组件简析

2020-09-29 17:48:15 浏览数 (1)

前言

之前说过了Jetpack架构组件,作为MVVM架构必备的组件,当然是人尽皆知了。然后jetpack还有很多其他可能被你忽视的组件,这次我们就说说其中一个同样精彩模块——行为组件。 还是老样子,通过举例的方式,让你掌握基本用法,心里有个数,走遍天下都不怕。

“行为组件可帮助您的应用与标准 Android 服务(如通知、权限、分享和 Google 助理)相集成。

Jetpack-行为组件

CameraX

“CameraX 是一个 Jetpack 支持库,旨在帮助您简化相机应用的开发工作。它提供一致且易于使用的 API Surface,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。 虽然它利用的是 camera2 的功能,但使用的是更为简单且基于用例的方法,该方法具有生命周期感知能力。它还解决了设备兼容性问题,因此您无需在代码库中添加设备专属代码。这些功能减少了将相机功能添加到应用时需要编写的代码量。

想必大家都了解过Camera APICamera2 API,总结就是两个字,不好用。哈哈,自我感觉,在我印象中,我要照相拍一张照片,不是应该直接调用一句代码可以完成吗。但是用之前的API,我需要去管理相机实例,设置SufraceView相关的各种东西,还有预览尺寸和图像尺寸,处理设置各种监听等等,头已晕。

可能是官方听到了我的抱怨,于是CameraX来了,CameraX是基于camera2进行了封装,给我们提供了更简单的解决方案来解决我们之前的困境。?来了

代码语言:javascript复制
    // CameraX core library using the camera2 implementation
    def camerax_version = "1.0.0-beta06"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    // If you want to additionally use the CameraX Lifecycle library
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    // If you want to additionally use the CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha13"
    // If you want to additionally use the CameraX Extensions library
    implementation "androidx.camera:camera-extensions:1.0.0-alpha13"
    
    
    <uses-permission android:name="android.permission.CAMERA" />
    
    //初始化相机
    private fun initCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            try {
                val cameraProvider = cameraProviderFuture.get()
                val preview = Preview.Builder().build()


                //图片拍摄用例
                mImageCapture = ImageCapture.Builder()
                    .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
                    .build()

                //配置参数(后置摄像头等)
                // Choose the camera by requiring a lens facing
                val cameraSelector =
                    CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_FRONT)
                        .build()

                //指定要与相机关联的生命周期,该生命周期会告知 CameraX 何时配置相机拍摄会话并确保相机状态随生命周期的转换相应地更改。
                val camera: Camera = cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    mImageCapture
                )

                //相机预览
                preview.setSurfaceProvider(view_finder.createSurfaceProvider())

            } catch (e: java.lang.Exception) {
                e.printStackTrace()
            }
        }, ContextCompat.getMainExecutor(this))
    }

    //拍照并保存
    fun takePhoto(view: View?) {
        if (mImageCapture != null) {
            val outputFileOptions: OutputFileOptions = OutputFileOptions.Builder(cretaeFile()).build()

            //拍照
            mImageCapture?.takePicture(
                outputFileOptions,
                ContextCompat.getMainExecutor(this),
                object : ImageCapture.OnImageSavedCallback {
                    override fun onImageSaved(@NonNull outputFileResults: OutputFileResults) {
                        //保存成功
                        Log.e(TAG, "success")
                    }

                    override fun onError(@NonNull exception: ImageCaptureException) {
                        //保存失败
                        Log.e(TAG, "fail")
                    }
                })
        }
    }    

使用起来挺方便吧,而且可以绑定当前activity的生命周期,这就涉及到另外一个组件Lifecycle了,通过一次绑定事件,就可以使相机状态随生命周期的转换相应地更改。 另外要注意的是先获取相机权限哦。

下载管理器

“DownloadManager下载管理器是一个处理长时间运行的HTTP下载的系统服务。客户端可以请求将URI下载到特定的目标文件。下载管理器将在后台执行下载,负责HTTP交互,并在失败或跨连接更改和系统重启后重试下载。

DownloadManager,大家应该都很熟悉吧,android2.3就开通提供的API,很方便就可以下载文件,包括可以设置是否通知显示,下载文件夹名,文件名,下载进度状态查询等等。?来

代码语言:javascript复制
class DownloadActivity : AppCompatActivity() {

    private var mDownId: Long = 0
    private var mDownloadManager: DownloadManager? = null
    private val observer: DownloadContentObserver = DownloadContentObserver()


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    //配置下载参数,enqueue开始下载
    fun download(url: String) {
        mDownloadManager =
            this.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val request = DownloadManager.Request(Uri.parse(url))
        // 设置文件夹文件名
        request.setDestinationInExternalPublicDir("lz_download", "test.apk")
        // 设置允许的网络类型
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
        // 文件类型
        request.setMimeType("application/zip")
        // 设置通知是否显示
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        //设置通知栏标题
        request.setTitle("apk download")
        //设置通知栏内容
        request.setDescription("*** apk")

        mDownId = mDownloadManager!!.enqueue(request)

 contentResolver.registerContentObserver(mDownloadManager!!.getUriForDownloadedFile(mDownId), true, observer)
    }

    //通过ContentProvider查询下载情况
    fun queryDownloadStatus(){
        val query = DownloadManager.Query()
        //通过下载的id查找
        //通过下载的id查找
        query.setFilterById(mDownId)
        val cursor: Cursor = mDownloadManager!!.query(query)
        if (cursor.moveToFirst()) {
            // 已下载字节数
            val downloadBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
            // 总字节数
            val allBytes= cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
            // 状态
            when (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) {
                DownloadManager.STATUS_PAUSED -> {
                }
                DownloadManager.STATUS_PENDING -> {
                }
                DownloadManager.STATUS_RUNNING -> {
                }
                DownloadManager.STATUS_SUCCESSFUL -> {
                    cursor.close()
                }
                DownloadManager.STATUS_FAILED -> {
                    cursor.close()
                }
            }

        }
    }

    //取消下载,删除文件
    fun unDownLoad(view: View?) {
        mDownloadManager!!.remove(mDownId)
    }


    override fun onDestroy() {
        super.onDestroy()
        contentResolver.unregisterContentObserver(observer)
    }


    //监听下载情况
    inner class DownloadContentObserver : ContentObserver(Handler(Looper.getMainLooper())) {
        override fun onChange(selfChange: Boolean) {
            queryDownloadStatus()
        }
    }

}

demo应该写的很清楚了,要注意的就是保存下载id,后续取消下载,查询下载进度状态都是通过这个id来查询。监听下载进度主要是通过观察getUriForDownloadedFile方法返回的uri,观察这个uri指向的数据库变化来获取进度。

媒体和播放

“Android 多媒体框架支持播放各种常见媒体类型,以便您轻松地将音频、视频和图片集成到应用中。

这里媒体和播放指的是音频视频相关内容,主要涉及到两个相关类:

  • MediaPlayer
  • ExoPlayer

MediaPlayer不用说了,应该所有人都用过吧,待会就顺便提一嘴。ExoPlayer是一个单独的库,也是google开源的媒体播放器项目,听说是Youtube APP所使用的播放器,所以他的功能也是要比MediaPlayer强大,支持各种自定义,可以与IJKPlayer媲美,只是使用起来比较复杂。

1)MediaPlayer

代码语言:javascript复制
        //播放本地文件
        var mediaPlayer: MediaPlayer? = MediaPlayer.create(this, R.raw.test_media)
        mediaPlayer?.start()

        //设置播放不息屏 配合权限WAKE_LOCK使用
        mediaPlayer?.setScreenOnWhilePlaying(true)


        //播放本地本地可用的 URI
        val myUri: Uri = Uri.EMPTY
        val mediaPlayer2: MediaPlayer? = MediaPlayer().apply {
            setAudioStreamType(AudioManager.STREAM_MUSIC)
            setDataSource(applicationContext, myUri)
            prepare()
            start()
        }

        //播放网络文件
        val url = "http://........"
        val mediaPlayer3: MediaPlayer? = MediaPlayer().apply {
            setAudioStreamType(AudioManager.STREAM_MUSIC)
            setDataSource(url)
            prepare()
            start()
        }


        //释放
        mediaPlayer?.release()
        mediaPlayer = null
    

2)ExoPlayer

代码语言:javascript复制
   compile 'com.google.android.exoplayer:exoplayer:r2.X.X'
   
    var player: SimpleExoPlayer ?= null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exoplayer)

        //初始化
        player = SimpleExoPlayer.Builder(this).build()
        video_view.player = player
        player?.playWhenReady = true

        //设置播放资源
        val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
            this,
            Util.getUserAgent(this, "yourApplicationName")
        )
        val uri: Uri = Uri.EMPTY
        val videoSource: MediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
            .createMediaSource(uri)
        player?.prepare(videoSource)
    }

    private fun releasePlayer() {
        //释放
        player?.release()
        player = null
    }

好像也不复杂?哈哈,更强大的功能需要你去发现。

通知

“通知是指 Android 在应用的界面之外显示的消息,旨在向用户提供提醒、来自他人的通信信息或应用中的其他实时信息。用户可以点按通知来打开应用,也可以直接在通知中执行某项操作。

这个应该都了解,直接上个?

代码语言:javascript复制
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "mychannel"
            val descriptionText = "for test"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
                description = descriptionText
            }
            // Register the channel with the system
            val notificationManager: NotificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }

    private fun showNotification(){
        val intent = Intent(this, SettingActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)

        val builder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("My notification")
            .setContentText("Hello World!")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            // Set the intent that will fire when the user taps the notification
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)

        with(NotificationManagerCompat.from(this)) {
            notify(1, builder.build())
        }

    }

权限

“权限的作用是保护 Android 用户的隐私。Android 应用必须请求权限才能访问敏感的用户数据(例如联系人和短信)以及某些系统功能(例如相机和互联网)。系统可能会自动授予权限,也可能会提示用户批准请求,具体取决于访问的功能。

权限大家应该也都很熟悉了。

  • 危险权限。6.0以后使用危险权限需要申请,推荐RxPermissions库
  • 可选硬件功能的权限。 对于使用硬件的应用,比如使用了相机,如果你想让Google Play允许将你的应用安装在没有该功能的设备上,就要配置硬件功能的权限为不必须的:
  • 自定义权限。这个可能有些同学没接触过,我们知道,如果我们设置Activity的exported属性为true,别人就能通过包名和Activity名访问我们的Activty,那如果我们又不想让所有人都能访问我这个Activty呢?可以通过自定义权限实现。?来
代码语言:javascript复制
//应用A
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.test.myapp" >
    
    <permission
      android:name="com.test.myapp.permission.DEADLY_ACTIVITY"
      android:permissionGroup="android.permission-group.COST_MONEY"
      android:protectionLevel="dangerous" />
    
     <activity
            android:name="MainActivity"
            android:exported="true" 
            android:permission="com.test.myapp.permission.DEADLY_ACTIVITY">
       </activity>
</manifest>

//应用B
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.test.otherapp" >
    
    <uses-permission android:name="com.test.myapp.permission.DEADLY_ACTIVITY" />
</manifest>

偏好设置

“建议使用 AndroidX Preference Library 将用户可配置设置集成至您的应用中。此库管理界面,并与存储空间交互,因此您只需定义用户可以配置的单独设置。此库自带 Material 主题,可在不同的设备和操作系统版本之间提供一致的用户体验。

开始看到这个标题我是懵逼的,设置?我的设置页官方都可以帮我写了?然后我就去研究了Preference库,嘿,还真是,如果你的App本身就是Material风格,就可以直接用这个了。但是也正是由于风格固定,在实际多样的APP中应用比较少。 来个?

代码语言:javascript复制

   implementation 'androidx.preference:preference:1.1.0-alpha04'
   
   //res-xml-setting.xml
   <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory
        app:key="notifications_category"
        app:title="Notifications">
        <SwitchPreferenceCompat
            app:key="notifications"
            app:title="Enable message notifications" />
    </PreferenceCategory>

    <PreferenceCategory
        app:key="help_category"
        app:title="Help">
        <Preference
            app:key="feedback"
            app:summary="Report technical issues or suggest new features"
            app:title="Send feedback" />

        <Preference
            app:key="webpage"
            app:title="View webpage">
            <intent
                android:action="android.intent.action.VIEW"
                android:data="http://www.baidu.com" />
        </Preference>
    </PreferenceCategory>
</PreferenceScreen>


class SettingFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.setting, rootKey)
        val feedbackPreference: Preference? = findPreference("feedback")

        feedbackPreference?.setOnPreferenceClickListener {
            Toast.makeText(context,"hello Setting",Toast.LENGTH_SHORT).show()
            true
        }
    }
}


class SettingActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_setting)

        supportFragmentManager
            .beginTransaction()
            .replace(R.id.settings_container, SettingFragment())
            .commit()
    }
    
}
   

首先新建xml文件,也就相当于设置页的布局了,包括那些分类,那些选项,以及选项的功能。 然后新建fragment继承自PreferenceFragmentCompat,这里就可以绑定xml文件,并且可以设置点击事件。 最后将fragment加到Activity即可。✌️

来张效果图看看

共享

“Android 应用的一大优点是它们能够互相通信和集成。如果某一功能并非应用的核心,而且已存在于另一个应用中,为何要重新开发它?

这里的共享主要指的是应用间的共享,比如发邮件功能,打开网页功能,这些我们都可以直接调用系统应用或者其他三方应用来帮助我们完成这些功能,这也就是共享的意义。

代码语言:javascript复制
    //发送方
    val sendIntent: Intent = Intent().apply {
        action = Intent.ACTION_SEND
        putExtra(Intent.EXTRA_TEXT, "This is my text to send.")
        type = "text/plain"
    }

    val shareIntent = Intent.createChooser(sendIntent, null)
    startActivity(shareIntent)
    
    //接收方
    <activity android:name=".ui.MyActivity" >
        <intent-filter>
            <action android:name="android.intent.action.SEND" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="text/plain" />
        </intent-filter>
    </activity>
        

切片

“切片是界面模板,可以在 Google 搜索应用中以及 Google 助理中等其他位置显示您应用中的丰富而动态的互动内容。切片支持全屏应用体验之外的互动,可以帮助用户更快地执行任务。您可以将切片构建成为应用操作的增强功能。

这个介绍确实有点模糊,但是说到Slice你会不会有点印象?2018年Google I/0宣布推出新的界面操作Action & Slice。而这个Slice就是这里说的切片。他能做什么呢?可以让使用者能快速使用到 app 里的某个特定功能。只要开发者导入 Slice 功能,使用者在使用搜寻、Google Play 商店、Google Assitant或其他内建功能时都会出现 Slice 的操作建议。

说白了就是你的应用一些功能可以在其他的应用显示和操作。

所以,如果你的应用发布在GooglePlay的话,还是可以了解学习下Slice相关内容,毕竟是Google为了应用轻便性做出的又一步实验。

怎么开发这个功能呢?很简单,只需要一步,右键New—other—Slice Provider就可以了。 slice库,provider和SliceProvider类都配置好了,方便吧。贴下代码:

代码语言:javascript复制
     <provider
          android:name=".slice.MySliceProvider"
          android:authorities="com.panda.jetpackdemo.slice"
          android:exported="true">
          <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.app.slice.category.SLICE" />
                <data
                    android:host="panda.com"
                    android:pathPrefix="/"
                    android:scheme="http" />
            </intent-filter>
        </provider>
        
        
class MySliceProvider : SliceProvider() {
    /**
     * Construct the Slice and bind data if available.
     * 切片匹配
     */
    override fun onBindSlice(sliceUri: Uri): Slice? {
        val context = context ?: return null
        val activityAction = createActivityAction() ?: return null
        return if (sliceUri.path.equals("/hello") ) {
            Log.e("lz6","222")
            ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .addRow(
                    ListBuilder.RowBuilder()
                        .setTitle("Hello World")
                        .setPrimaryAction(activityAction)
                )
                .build()
        } else {
            // Error: Path not found.
            ListBuilder(context, sliceUri, ListBuilder.INFINITY)
                .addRow(
                    ListBuilder.RowBuilder()
                        .setTitle("URI not found.")
                        .setPrimaryAction(activityAction)
                )
                .build()
        }
    }

    //切片点击事件
    private fun createActivityAction(): SliceAction? {
        return SliceAction.create(
            PendingIntent.getActivity(
                context, 0, Intent(context, SettingActivity::class.java), 0
            ),
            IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground),
            ListBuilder.ICON_IMAGE,
            "Open App"
        )
    }

}
        

如上就是切片的重要代码,其中onBindSlice是用来匹配uri的,比如上述如果uri为/hello就显示一个ListBuilder。createActivityAction方法则是响应切片点击事件的。 可以看到在AndroidManifest.xml中是通过provider配置的,所以这个切片的原理就是通过ContentProvider形式,让外部可以访问这个provider,然后响应相关事件或者显示相关的view。

好了,接下来就是测试切片使用了,完整的切片URI是slice-content://{authorities}/{action},所以这里对应的就是slice-content://com.panda.jetpackdemo.slice/hello

又在哪里可以使用呢?官方提供了一个可供测试的app—slice-viewer。 下载下来后,配置好URI,就会提示要访问某某应用的切片权限提示,点击确定就可以看到切片内容了(注意最好使用模拟器测试,真机有可能无法弹出切片权限弹窗)。如下图,点击hello就可以跳转到我们之前createActivityAction方法里面设置的Activity了。

slice.jpg

总结

Jetpack-行为组件讲完了,这部分主要是和Android服务相结合的一些库,帮助大家更好的调用系统服务。 有些组件可能大家很少可能会用到,比如切片,但是不得不承认切片的想法真的很好,有时候我们就可能只用应用中的某一个小功能,但是又要打开app才能使用。有了切片就可以随时随地快速用这些小功能了。 咦,这不就是小程序?哈哈哈,wx?。但是毕竟小程序是有限的,如果所有的app都能集成这个功能,手机也支持了,那不就更好吗?

Android开发者们,快来关注公众号【码上积木】,每天三问面试题,并详细剖析,助你成为offer收割机。

积累也是一种力量。

附件

[Jetpack实践官方Demo—Sunflower]:(https://github.com/android/sunflower) [文章相关所有Demo]:(https://github.com/JiMuzz/jimu-Jetpack-Demo)

0 人点赞