《第一行代码》第三版之通知、多媒体(十)

      本章介绍了通知及使用技巧、调用摄像头及读取相册、播放音视频。最后我们介绍了infix函数这种高级语法糖的用法。
9.1.将程序运行到手机上
      没啥好讲的
9.2.使用通知
      某app不在前台运行时却希望向用户发出一些提示信息,可以借助通知来实现。发出通知后,最上方状态栏会显示一个通知的图标,下拉状态栏可以获取通知的详细内容。

       //第一步:getSystemService用于获取系统的那个服务,需要一个NotificationManager对同志进行管理
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        //第二步:创建通知渠道,低于8.0无法创建通知渠道
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            //构建一个通知渠道,创建的话需要知道渠道ID、渠道名称和重要等级。渠道ID随便定义,保证全局唯一性
            //渠道名称给用户看,清楚表明用途,重要等级有四种。IMPORTANT_HIGH、DEFAULT、LOW、MIN。
            val channel = NotificationChannel(channelID,channelName,importance)
            //完后创建通知渠道
            manager.createNotificationChannel(channel)
        }

9.2.1.创建通知渠道
       每个app都乱发送通知,用户烦不胜烦。要么同意接收所有信息,要么屏蔽所有信息,这也是Android通知功能的痛点。因此Android8.0引入通知渠道的概念。
       通知渠道是每条通知都要属于一个相应的渠道。每个app可自由创建当前应用拥有哪些通知渠道,但这些通知渠道的控制权是掌握在用户手上的,用户可以选择这些通知渠道重要程度,是否响铃、震动或者关闭。譬如微博可以创建两种通知渠道,一个关注、一个推荐。
      创建通知渠道的代码如下:

      //第一步:getSystemService用于获取系统的那个服务,需要一个NotificationManager对同志进行管理
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        //第二步:创建通知渠道,低于8.0无法创建通知渠道
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            //构建一个通知渠道,创建的话需要知道渠道ID、渠道名称和重要等级。渠道ID随便定义,保证全局唯一性
            //渠道名称给用户看,清楚表明用途,重要等级有四种。IMPORTANT_HIGH、DEFAULT、LOW、MIN。
            val channel = NotificationChannel(channelID,channelName,importance)
            //完后创建通知渠道
            manager.createNotificationChannel(channel)
        }

9.2.2.通知的基本用法
      可以在Service、BroadCastReceiver和Activity中创建,前两者较多,步骤相同。AndroidX提供的兼容API提供了NotificationCompat类用以创建Notification对象以保证所有系统版本均可运行:

      //第一个参数是context,第二个参数是渠道ID,可以连缀任意多的方法来创建一个丰富的Notification对象
        val notification = NotificationCompat.Builder(context,channelId).build()
        //让通知显示出来,第一个参数是ID,每个通知指定的id不同,第二个参数是Notification对象
        manager.notify(1,notification)

      创建NotificationTest项目:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/send_Notice"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send Notice" />

</LinearLayout>
package com.example.myapplication

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.app.NotificationCompat
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //步骤一:获取NotificationManager实例
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        //步骤二:建立通知渠道
        if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
            val channel = NotificationChannel("normal","Normal",NotificationManager.IMPORTANCE_DEFAULT)
            manager.createNotificationChannel(channel)
        }
        //步骤三:点击事件里完成通知的创建工作
        send_Notice.setOnClickListener {
            //build方法之前连缀任意多的方法创建一个丰富的Notification对象,基本设置包括:
            //setContentTitle标题内容;setContentText文本内容;setSmallIcon设置通知的小图标,纯alpha图层;setLargeIcon大图标
            val notification = NotificationCompat.Builder(this,"normal")
                    .setContentTitle("This is content title")
                    .setContentText("This is content text")
                    .setSmallIcon(R.drawable.small_icon)
                    .setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
                    .build()
            //让通知显示出来,一个是id,一个是notification对象。
            manager.notify(1,notification)
        }
    }
}

      仅仅显示可不行,点击通知的效果要有啊,涉及到PendingIntent,延迟执行的Intent。可以通过getActivity、getBroadcase和getService几个方法来获取PendingIntent实例。新建NotificationActivity这一Activity。修改xml和点击事件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="This is Notification layout"
        android:textSize="24sp" />
</RelativeLayout>
        send_Notice.setOnClickListener {
            val intent = Intent(this, NotificationActivity::class.java)
            //第一个参数是Context,第二个参数用不到,第三个参数时Intnet对象,通过这个对象构建出PendingIntent的意图;第四个参数是PendingIntent的行为,FLAG_ONE_SHOT等
            val pi = PendingIntent.getActivity(this, 0, intent, 0)
            //build方法之前连缀任意多的方法创建一个丰富的Notification对象,基本设置包括:
            //setContentTitle标题内容;setContentText文本内容;setSmallIcon设置通知的小图标,纯alpha图层;setLargeIcon大图标
            //setContentIntent设置延迟Intent;setAutoCancel自动取消掉
            val notification = NotificationCompat.Builder(this, "normal")
                .setContentIntent(pi)
                .setAutoCancel(true)
                .setContentTitle("This is content title")
                .setContentText("This is content text")
                .setSmallIcon(R.drawable.small_icon)
                .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
                .build()
            //让通知显示出来,一个是id,一个是notification对象。
            manager.notify(1, notification)
        }

      当然可以将setAutoCancel(true)改为修改NotificationActivity里面的内容:

class NotificationActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_notification)
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.cancel(1)
    }
}

9.2.3.通知的进阶技巧
      可以使用setStyle取代setContentText,因为后者长文本时不能完全显示,后面的都会被省略。

 val notification = NotificationCompat.Builder(this, "normal")
                .setContentIntent(pi)
                .setContentTitle("This is content title")
                .setStyle(NotificationCompat.BigTextStyle().bigText("豫章故郡,洪都新府。星分翼轸,地接衡庐。襟三江而带五湖,控蛮荆而引瓯越。物华天宝,龙光射牛斗之墟;人杰地灵,徐孺下陈蕃之榻。雄州雾列,俊采星驰。台隍枕夷夏之交,宾主尽东南之美。都督阎公之雅望,棨戟遥临;宇文新州之懿范,襜帷暂驻。十旬休假,胜友如云;千里逢迎,高朋满座。腾蛟起凤,孟学士之词宗;紫电青霜,王将军之武库。家君作宰,路出名区;童子何知,躬逢胜饯。"))
                .setSmallIcon(R.drawable.small_icon)
                .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
                .build()

      也可以显示大图片。

 //BitmapFactory.decodeResource将图片解析为Bitmap对象,在传入BigPicture中
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources, R.drawable.big_image)))

       也可以调整优先级:

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel2 =
                NotificationChannel("important", "Important", NotificationManager.IMPORTANCE_HIGH)
            manager.createNotificationChannel(channel2)
        }
      ...
      val notification = NotificationCompat.Builder(this, "important")

9.3.调用摄像头和相册
     新建CameraAlbumTest项目,修改布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/take_photo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Take Photo" />

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />
</LinearLayout>

     修改MainActivity:

package com.example.myapplication

import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import androidx.core.content.FileProvider
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File

class MainActivity : AppCompatActivity() {
    val takePhoto = 1
    lateinit var imageuri: Uri
    lateinit var outputimage: File
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        take_photo.setOnClickListener {
            //创建File对象,用于存储拍照后的照片,存储位置是SD卡的应用关联缓存目录下,6.0后读写SD卡是危险权限,使用关联目录cache可以跳过这一步。Android10.0后使用作用域存储。
            outputimage = File(externalCacheDir, "output_image.jpg")
            //如果File已经存在,删掉,并调用createNewFile创建新文件
            if (outputimage.exists()) {
                outputimage.delete()
            }
            outputimage.createNewFile()

            imageuri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                //如果系统版本大于7.0,本地真实路径uri不安全,会抛出异常。 FileProvider.getUriForFile可以将File对象转换为一个封装后的uri对象。
                //getUriForFile接收三个参数,一个是context,另一个是任意字符串,第三个是File对象。FileProvider使用类似ContentProvider的机制进行保护,提高程序安全性。
                FileProvider.getUriForFile(this, "com.example.camera.fileprovider", outputimage)
            } else {
                //如果系统版本低于7.0,调用Uri.fromFile将File对象转换为Uri对象,这个对象是本地真实路径
                Uri.fromFile(outputimage)
            }
            //Intent的action进行指定,Intent的putExtra指定图片的输出地址,刚刚得到uri对象
            val intent = Intent("android.media.action.IMAGE_CAPTURE")
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imageuri)
            //启动Activity,隐式的。调用之后返回到onActivityResult方法。
            startActivityForResult(intent, takePhoto)
        }
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            takePhoto -> {
                //将拍摄的照片显示出来,拍照成功的话使用BitmapFactory.decodeStream将图片解析为bitmap对象
                val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageuri))
                //最后设置到ImageView当中,再加上照片旋转的处理。
                image_view.setImageBitmap(rotateIfRequired(bitmap))
            }
        }
    }

    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 rotatedBitmap =
            Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        bitmap.recycle()//不再需要的bitmap对象回收
        return rotatedBitmap
    }
}

     修改AndroidManifest.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- android:name值固定,android:authorities与刚才第二个参数一致;另外provider标签的内部使用<meta-data>来指定Uri的共享路径,并引用了一个@xml/file_paths资源-->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.camera.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>

</manifest>

     新建xml目录,新建file_paths.xml。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- external-path用来指定uri共享的,name的值随便填,path的值表示共享的具体路径,用单斜线将SD卡进行共享-->
    <external-path
        name="my_images"
        path="/" />
</paths>

9.3.2.从相册中选择图片
     布局就不说了,一个button组件。主要是MainActivity里的修改。

class MainActivity : AppCompatActivity() {
    val takePhoto = 1
    val fromAlbum = 2
    lateinit var imageuri: Uri
    lateinit var outputimage: File
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ....
        from_album_btn.setOnClickListener {
            //1.打开文件选择器,Intent的action指定为打开系统文件选择器。
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
            //2.指定只显示图片,增加过滤条件,只允许打开图片文件显示出来
            intent.addCategory(Intent.CATEGORY_OPENABLE)
            intent.type = "image/*"
            //3.选择完图片后进入onActivityResult方法
            startActivityForResult(intent, fromAlbum)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            ...
            fromAlbum -> {
                //如果data不等于null
                if (resultCode == Activity.RESULT_OK && data != null) {
                    //调用Intnet的getData方法获取图片的uri。在调用getBitmapFromUri将uri转换为Bitmap对象,最后显示出来。
                    data.data?.let { uri ->
                        val bitmap = getBitmapFromUri(uri)
                        image_view.setImageBitmap(bitmap)
                    }
                }
            }
        }
    }

    private fun getBitmapFromUri(uri: Uri) = contentResolver.openFileDescriptor(uri, "r")?.use {
        BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
    }
   .....
}

9.4.播放多媒体文件
9.4.1.播放音频

     音频文件MediaPlayer类中的方法:

                                          
     新建PlayAudioTest项目,新建assets目录用于存储音乐文件,修改布局文件activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/play_music_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Play"/>

    <Button
        android:id="@+id/pause_music_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Pause"/>

    <Button
        android:id="@+id/stop_music_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop"/>
</LinearLayout>

     修改MainActivity里的代码:

package com.example.myapplication

import android.media.MediaPlayer
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    //类初始化创建一个MediaPlayer的实例
    private val mediaPlayer = MediaPlayer()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //初始化
        initMediaPlayer()
        play_music_btn.setOnClickListener {
            //对于细节的处理堪称完美,来了先判断,用完之后初始化、销毁等等
            if (!mediaPlayer.isPlaying){
                mediaPlayer.start()
            }
        }
        pause_music_btn.setOnClickListener {
            if (mediaPlayer.isPlaying){
                mediaPlayer.pause()
            }
        }
        stop_music_btn.setOnClickListener {
            if (mediaPlayer.isPlaying){
                //重置为刚才的状态并重现调用initMediaPlayer方法
                mediaPlayer.reset()
                initMediaPlayer()
            }
        }
    }
    private fun initMediaPlayer(){
        //得到一个assetManager实例,assetManager可读取assets目录下的所有资源
        val assetManager = assets
        //调用openFd将音频文件句柄打开,做一次调用setDataSource设置要播放文件的位置、prepare完成初始化
        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()
    }
}

9.4.2.播放视频
     借助VideoView类来实现,VideoView并不是万能工具类,其支持的格式不多、效率低。视频放在新建的raw目录下,方法如下:

                                                    
      新建PlayVideoTest项目,修改activity_main.xml。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/play_video_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Play" />

        <Button
            android:id="@+id/pause_video_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Pause" />

        <Button
            android:id="@+id/stop_video_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Replay" />
    </LinearLayout>

    <VideoView
        android:id="@+id/video_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

      修改MainActivity.java:

package com.example.myapplication

import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //将raw目录下的video.MP4解析为一个uri对象
        val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
        //调用setVideoURI将解析出来的uri对象传入,这样完成了初始化
        video_view.setVideoURI(uri)
        play_video_btn.setOnClickListener {
            if (!video_view.isPlaying){
                video_view.start()
            }
        }
        pause_video_btn.setOnClickListener {
            if (video_view.isPlaying){
                video_view.pause()
            }
        }
        stop_video_btn.setOnClickListener {
            if (video_view.isPlaying){
                video_view.resume()//重新播放
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        video_view.suspend()
    }
}

9.5.Kotlin课堂:使用infix函数构建更可读的用法

   val map = mapOf("Apple" to 1, "Banana" to 2, "Pear" to 3)
   for ((fruit, number) in map) {
         println("fruit is " + fruit + ",number is " + number)
   }

      to并不是Kotlin关键字,借助了高级语法糖:infix函数,infix只是调整了写法,他是将A .to( B)改为A to B。Infix可提高代码的可读性。举例来讲:

//判断字符串是否以某个参数开头
if ("Hello Kitty".startsWith("Hello")) {
           //处理逻辑  
}
//将其改写为infix函数形式:
//借助infix函数,使用更可读的写法
if ("Hello Kitty" beginsWith "hello"){
}
//String类的扩展函数,添加一个beginsWith,内部实现基于startsWith方法。
// 加上infix后beginsWith变为infix函数,除了传统的调用方式,还有特殊语法糖格式
infix fun String.beginsWith(prefix: String) = startsWith(prefix)

     Infix函数需要满足两个条件:1.不能定义为顶层函数,必须为类成员函数或者扩展函数;2.只能接受一个参数,参数类型没有限制。举例来说:

    val list2 = listOf("Apple", "Banana", "Orange", "Pear")
    if (list2.contains("Banana")) {
         //处理具体逻辑
     }
    //使用infix写法
    if (list2.has("Banana")) {
        //处理具体逻辑
    }
    //给所有Collection接口添加一个扩展函数,这是因为Collection是所有Java和Kotlin集合的总接口,因此给它添加一个has函数所有集合子类都能用了
    infix fun <T> Collection<T>.has(element: T) = contains(element)

     研究A to B,发现是使用了Pair函数,自己仿写整一个:

 val map = mapOf("Apple" with 1, "Banana" with 2, "Pear" with 3)
 infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张云瀚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值