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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341