Kotlin的Android多媒体探究(五)

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函数用法