Kotlin的Android多媒体探究(五)

作者: 阴天吃鱼 | 来源:发表于2020-05-29 15:02 被阅读0次

    1、通知
    2、调用摄像头和相册
    3、播放音频、视频
    4、infix函数

    • 1、通知

    通知就是当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。
    ——
    每发出一条通知,就意味着自己的应用程序有着更高的打开率,因此有太多的应用会想尽办法给用户发送通知,虽然Android系统有禁止通知的功能,但也许有些通知是需要用户关心的,
    ——
    于是在Android8.0就引入了通知渠道这一概念。就是每条通知都要属于一个对应的渠道,每个用户可以自由的创建当前应用的通知渠道,从而控制这些通知渠道的重要程度,是否响铃震动或者关闭等等。

    以下代码等均为android.x版本

    class TestActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
    
            val notificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel =
                    NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
                notificationManager.createNotificationChannel(channel)
            }
    
            button.setOnClickListener {
                val notify = NotificationCompat.Builder(this, "normal")
                    .setContentTitle("this is content title")
                    .setContentText("this is content text")
                    .setSmallIcon(R.drawable.ic_login_user)
                    .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_login_pwd))
                    .build()
                notificationManager.notify(1, notify)
            }
    
        }
    }
    

    创建了通知渠道和通过点击发送一条通知。
    用NotificationCompat是因为之前的Android各api的通知都有部分改动,而Android.x整合到了一起,并且提供NotificationCompat适配低版本。

    ——
    在正常显示通知之后, 你会发现这条通知并没有点击效果,那怎么能让它具有点击效果呢, 如下:

    新建一个通知跳转页面

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.hdsx.guangxihighway.ui.welcome.PendActivity">
    
        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="this is notification layout"
            android:textSize="30dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    

    之后只需要再原来的通知条件上增加Intent就可以

    class TestActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
    
            val notificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel =
                    NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
                notificationManager.createNotificationChannel(channel)
            }
    
            button.setOnClickListener {
                val intent = Intent(this, PendActivity::class.java)
                val p = PendingIntent.getActivity(this, 0, intent, 0)
    
                val notify = NotificationCompat.Builder(this, "normal")
                    .setContentTitle("this is content title")
                    .setContentText("this is content text")
                    .setSmallIcon(R.drawable.ic_login_user)
                    .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_login_pwd))
                    .setContentIntent(p)
                    .setAutoCancel(true)
                    .build()
                notificationManager.notify(1, notify)
            }
    
        }
    }
    

    setContentIntent设置跳转Intent,
    而setAutoCancel的作用是 点击了这条通知后让其自动取消掉。
    当然也可以直接通过manager.cancel(标识)取消

    ———
    而通知并不是只有这些,如果想要丰富通知的内容,不让其显示的那么单调,可以通过setStyle方法来构建。具体可以看看API等等,此处不过多讲解。

    ——
    需要注意的是通知的重要程度,
    通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。比如高重要等级的通知可以发出横幅,发出声音。而低等级的通知不仅可能会在某些情况下被隐藏,而且可能会被改变显示顺序。
    当然这也不代表开发者就可以随心所欲了,开发者只能在创建通知渠道的时候为它指定初始的重要等级。如果用户对其不认可的话,可以随时进行修改。而开发者对此修改无权干涉。

    举例就类似微信,你正在别的App操作,来了一个消息给你推出了一个横幅。你可以对这个横幅进行控制。

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel =
                    NotificationChannel("important", "Important", NotificationManager.IMPORTANCE_HIGH)
                notificationManager.createNotificationChannel(channel)
            }
    

    .
    .
    .

    • 2、调用摄像头和相册

    假设应用要求用户上传一张图片作为头像,这时打开摄像头直接拍照是最为简单便捷的,那怎么能在应用内完成这一操作呢?

    ———

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.hdsx.guangxihighway.ui.welcome.TestActivity">
    
        <Button
            android:id="@+id/btnTakePhoto"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="take photo" />
    
        <ImageView
            android:id="@+id/imageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal" />
    </LinearLayout>
    
    

    拍照则有按钮和显示,声明布局文件

    class TestActivity : AppCompatActivity() {
    
        //请求的code
        val takePhoto = 1
    
        //
        lateinit var imageUri: Uri
    
        //用来存放摄像头拍下的图片
        lateinit var outputImage: File
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
    
            btnTakePhoto.setOnClickListener {
                //externalCacheDir获取应用的缓存目录。
                //从Android6.0开始读写Sd卡就列为了危险的权限,访问的话需要运行时权限。而缓存目标即可跳转这个权限
                //在Android10系统开始,公用的Sd卡目录已经不允许直接访问了,而是要是用作用域存储才行。具体见文章:https://mp.weixin.qq.com/s/_CV68KeQolJQqvUFo10ZVw
                outputImage = File(externalCacheDir, "output_image.jpg")
                if (outputImage.exists()) {
                    outputImage.delete()
                }
                outputImage.createNewFile()
    
                //而这块主要是7.0的版本特性// 7.0的真实路径的Uri是被认为不安全的。
                // 所以要通过特殊的Contentprovider来进行保护
                imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    FileProvider.getUriForFile(
                        this,
                        "com.hdsx.guangxihighway.fileprovider",
                        outputImage
                    )
                } else {
                    Uri.fromFile(outputImage)
                }
    
                //启动相机程序
                val intent = Intent("android.media.action.IMAGE_CAPTURE")
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
                startActivityForResult(intent, takePhoto)
            }
        }
    
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            when (requestCode) {
                takePhoto ->
                    if (resultCode == Activity.RESULT_OK) {
                        val bitmap =
                            BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
                        imageView.setImageBitmap(rotateIfRequired(bitmap))
                    }
            }
        }
    
        /**
         * 因为拍照有可能存下一些照片旋转的问题,如果横屏的话,那照片拍出来是横屏的,回归到竖屏的话就会有90度的旋转
         * 而前置和后置摄像头旋转的度数也不同、
         */
        private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
            val exif = ExifInterface(outputImage.path)
            val orientation =
                exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
            return when (orientation) {
                ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
                ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
                ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
                else -> bitmap
            }
        }
    
        /**
         * 调整旋转角度
         */
        private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
            val matrix = Matrix()
            //用指定的旋转对矩阵进行后处理
            matrix.postRotate(degree.toFloat())
            val rotateBitmap =
                Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
            bitmap.recycle()
            return rotateBitmap
        }
    
    }
    

    关于一些参数的使用和特性,我都在注释里做了讲解。

    ——

    7.0的特性文件,如下:

            <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="com.hdsx.guangxihighway.fileprovider"
                android:exported="false"
                android:grantUriPermissions="true">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths" />
            </provider>
    

    @xml - 在res目录下新建个xml文件夹,并且新增Xml

    <?xml version="1.0" encoding="utf-8"?>
    <xml xmlns:android="http://schemas.android.com/apk/res/android">
        <external-path
            name="my_images"
            path="/" />
    </xml>
    

    external-path就是用来指定Uri共享的,name属性可以随便填写
    而"/"表示将整个SD卡进行共享,当然你也可以仅共享存在output_image.jpg这张图片的路径。

    ————
    当然拍照方便,但如果手机本身就有很多张图片,我不需要拍照,直接调用相册就可以。

    ————
    相册选择图片,
    布局 ,新增一个按钮用来做选择图片的操作

        <Button
            android:id="@+id/btnSelectPhoto"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="select photo" />
    
    btnSelectPhoto.setOnClickListener {
                val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
                intent.addCategory(Intent.CATEGORY_OPENABLE)
                intent.type = "image/*"
                startActivityForResult(intent, selectPhoto)
            }
    

    修饰type为image类型,

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            when (requestCode) {
              selectPhoto ->
                    if (resultCode == Activity.RESULT_OK && data != null) {
                        data.data?.let {
                            val bitmap = getBitmapFromUri(it)
                            imageView.setImageBitmap(bitmap)
                        }
                    }
            }
        }
    
      private fun getBitmapFromUri(uri: Uri) = contentResolver.openFileDescriptor(uri, "r")?.use {
            BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
        }
    

    ————

    • 3、播放音频、视频

    在Android中播放音频一般是由MediaPlayer类实现的


    MediaPlayer常见的方法.png

    来操作一下:
    在Assets文件夹下放置 music.mp3
    在当前页面新增三个按钮来控制音频的状态

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.hdsx.guangxihighway.ui.welcome.TestActivity">
    
        <Button
            android:id="@+id/paly"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="paly" />
    
        <Button
            android:id="@+id/pause"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="pause" />
    
        <Button
            android:id="@+id/stop"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="stop" />
        
    </LinearLayout>
    
    

    准备做好后,接着书写逻辑代码

    class TestActivity : AppCompatActivity() {
    
        private val mediaPlayer = MediaPlayer()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
    
            initMediaPlayer()
    
            play.setOnClickListener {
                if (mediaPlayer.isPlaying) mediaPlayer.start()
            }
    
            pause.setOnClickListener {
                if (mediaPlayer.isPlaying) mediaPlayer.pause()
            }
    
            stop.setOnClickListener {
                if (mediaPlayer.isPlaying) {
                    mediaPlayer.reset()
                    initMediaPlayer()
                }
            }
    
        }
    
        private fun initMediaPlayer() {
            //播放之前的准备工作
            val assetManager = assets
            val fd = assetManager.openFd("music.mp3")
            mediaPlayer.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
            mediaPlayer.prepare()
        }
    
        override fun onDestroy() {
            super.onDestroy()
            mediaPlayer.stop()
            mediaPlayer.release()
        }
    
    }
    

    ————

    播放视频
    视频的播放是由VideoView类来实现的。


    VideoView常见方法.png

    在res资源下新建raw文件夹,并且放入video.mp4视频文件
    接着创建布局文件,

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.hdsx.guangxihighway.ui.welcome.TestActivity">
    
        <Button
            android:id="@+id/play"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="play" />
    
        <Button
            android:id="@+id/pause"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="pause" />
    
        <Button
            android:id="@+id/replay"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="replay" />
    
    </LinearLayout>
    
    

    准备做好后,接着书写代码

    class TestActivity : AppCompatActivity() {
    
        private val videoView = VideoView(this)
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
    
            val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
            videoView.setVideoURI(uri)
    
            play.setOnClickListener {
                if (videoView.isPlaying) videoView.start()
            }
    
            pause.setOnClickListener {
                if (videoView.isPlaying) videoView.pause()
            }
    
            replay.setOnClickListener {
                if (videoView.isPlaying) videoView.resume()
            }
    
        }
    
        override fun onDestroy() {
            super.onDestroy()
            videoView.suspend()
        }
    
    }
    
    

    ————

    • 4、infix函数

    举个例子,我通过mapOf创建一个map

    val map = mapOf("a" to 1, "b" to 2, "c" to 3)
    

    我们能发现在构建键值对的时候 直接使用的 to,那 to是不是kotlin的一个函数呢,答案肯定是: 不是。其实也可以看成 A.to(B) 只不过是省略了小数点和括号。

    在比如:

    if ("Hello World".startsWith("Hello")) {
                
    

    这肯定是包含的,我们加上infix函数

    //1
    infix fun String.begin(str: String) = startsWith(str)
    
    //2
    if ("Hello World".begin("Hello")) {
    
    //3
    if ("Hello World" begin "Hello") {
    
            
    

    随着递进是不是发现了被infix修饰的高阶函数, 可以省略掉 (小数点和括号)。它就是这样的作用,非常特殊。
    所以对其使用有比较 严格的两个限制:
    (1)infix函数是不能定义变成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方法将它定义到某个类中
    (2)它必须只能有一个接收参数,参数类型没有限制。

    在比如list:

    //1
    val list = listOf("1", "2", "3")
    if (list.contains("1")){}
    
    
    //2
    infix fun <T> Collection<T>.has(element: T) = contains(element)
    
    //3
    if (list has "1") {}
    

    最后在说下Map to的实现:

    //1
    val map = mapOf("a" to 1, "b" to 2, "c" to 3)
    //2
    infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)
    //3
    val map = mapOf("a" with  1, "b" with  2, "c" with  3)
    

    总结
    多媒体的简单应用
    infix函数用法

    相关文章

      网友评论

        本文标题:Kotlin的Android多媒体探究(五)

        本文链接:https://www.haomeiwen.com/subject/cuztzhtx.html