Android – 运用手机多媒体

9、运用手机多媒体

很早以前,手机的功能普遍比较单调,仅仅就是用来打电话和发短信的,而如今,手机在我们的生活正扮演着越来越重要的角色,各种娱乐活动都可以在手机上进行。

手机上众多的娱乐方式省不了强大的多媒体功能的支持,而Android在这方面做得非常出色,它提供了一系列的API,使得我们可以在程序中调用很多手机的多媒体资源,从而缩写出更加丰富的多彩的应用程序。

9.1将程序运行到手机上

我们首先得有一台Android手机。

想要将程序运行到手机上,我们需要先通过数据线把手机连接到电脑上,然后进入设置、系统、开发都选项界面,并在这个界面中选中USB调试,如图

注意:从Android4.2系统开始,开发者选项默认是隐藏的,你需要先进入 “关于手机“ 界面,然后对手机最下面的版本号一样连续点击,就会主开发者选项显示出来。

9.2 使用通知

通知(notifcation)是Android系统中比较有特色的一个功能,当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现,发出一条通知后,手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。

9.2.1 创建通知渠道

通知这个功能的设计初衷是好的,后来动被开发者给玩坏了。

每发出一条通知,都 可以意味着自己的应用程序会拥有更高的打开率,因此有太多太多的应用 会想尽办法地给用户改善通知,以博取更多的展示机会,站在应用自身的角度来看,这么做或许并没有什么 错,但是站在用户的角度来看,如果每一个应用程序都这么做的话,那么用户手机的状态栏会被各式各样的通信信息堆满。

虽然Android系统允许我们将某个应用程序的通知完全屏蔽,以防止它一直给我们改善垃圾信息,但是在这些信息中,也可以有我们所关心的内容,比如说我们希望收到到某个我所关注的人给我发来的信息,即不想接收官方给我推送的一些不重要的信息。在过去,用户是有办法对这些信息做区分的,要么两间接受所有信息,要么屏蔽所有信息,这就是Android通知的痛点。

于是,Android8.0系统引入了通知渠道这个概念。

什么是通知渠道?顾名思义,就是每条通知都 要属于一个对应的渠道,每个应用程序都 可以以自由地创建当前应用拥有哪些通知渠道,但是这引起通知渠道的控制权是掌握在用户手上的,用户以自由地选择这些通知渠道的程度,是否响铃,是否振动或者是否要关渠道的通知。

拥有了这些控制权之后,用户就再也不用害怕那些垃圾通知的打扰了,因为用户可以自主地选择关心哪些通知,不关心哪些通知,以刚才的场景,皮皮虾可以创建两种通知渠道,一个关注,一个推荐,而我作为用户,如果对推荐类的通知不感兴趣,那么我就可以直接将推荐通知渠道关闭,这样既不影响我接收关心的通知,又不会让那些我不关心的通知来打扰我了。

首选需要一个NotifcationManger对通知进行管理,可以通过调用Context的getSystemService()方法获取,getSystemService()方法接收一个字符串参数用于确定获取系统的哪个服务。这里我们传入Context.NOTIFICATION_SERVICE即可。因此,获取NotificationManger的实例就可以写成:

        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

接下来要使用NotificationChnnel类构建不念旧恶通知渠道,立项调用NotificationManger的createNotificationChannel()方法完成创建。由于NotificationChannel类和createNotificationChannel()方法都是Android8.0系统中新增的API,因此我们在使用的时候还需要进行版本判断才可以,写法如下:

        manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
           NotificationChannel channel = new NotificationChannel(
                   "nsyw", "测试通知", NotificationManager.IMPORTANCE_HIGH
          );
           manager.createNotificationChannel(channel);
      }

创建一个通知渠道至少需要渠道ID,渠道名称以及重要等级这3个参数,其中渠道ID可以自定义,只要保证合唯一即可。渠道名称是给用户看的,需要可以清楚地表达这个渠道的用途。通知的重要等级主要有IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、IMPORTANCE_LOW、IMPORTANCE_MIN这几种,对应的重要程度依次从高到低。不同的重要等级会商定通知的不同行为

重要程度描述
IMPORTANCE_MIN开启通知,不会弹出,不发声,状态栏不显示
IMPORTANCE_LOW开启通知,不会弹出,不发声,状态栏显示
IMPORTANCE_DEFAULT开启通知,不会弹出,可发声,状态栏显示
IMPORTANCE_HIGH开启通知,可弹出,可发声,状态栏显示

9.2.2 通知 的基本用法

通知的用法还是比较灵活的,可以在Activity里创建,也可以在BroadcastReceiver里创建,也可以在Service里创建,相比于BroadcastReceiver和Service。在Activity里创建通知的场景还是比较少的,因为只有当程序进入后台的时候才需要使用通知。

不过,无论是在哪样里创建通知,整体的步骤都是相同的。

首先需要使用一个Builer构造器来创建Notification对象,但问题在于,Android系统的每一个版本都会对通知功能或多或少的修改,API不稳定的问题在通知上显示尤其严重,比方说刚刚介绍的通知渠道功能在Android8.0系统之前就是没有的,那么该如何解决这个问题呢?其实解决方案我们之前已经见过好几回了,就是使用AndroidX库中提供的兼容API。AndroidX库中提供了一个NotificationCompat类,使用这个类的构造器创建Notification对象,就可以保证我们的程序在所有Android系统版本上都能正常工作,代码如下:

Notification.Builder nsyw = new Notification.Builder(this, "nsyw");

Notification.Builder的构造中接收两个参数:第一个参数是Context;第二个参数是渠道ID,必须和通知渠道ID保持一致才行。

当然,只创建了一个空的Notification对象,并没有什么实际作用,我们

new NotificationCompat.Builder(this, "nsyw")
               // 设置通知标题
              .setContentTitle("我是标题")
               // 设置通知内容
              .setContentText("年少有为")
               // 添加小图标
              .setSmallIcon(R.drawable.ic_baseline_close_24)
              .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.image))
              .build();

上方代码中一共调用了4个方法,setContentTitle()方法用于指定通知的标题内容,下拉状态栏就可以看到这部分内容。setContentText()方法 用于指定的正文内容,同样下拉系统状态栏就可以看到。setSmallIcon()方法用于设置通知的小图标。注意,只能使用纯alpha图层的图片进行设置,小图标会显示在系统状态栏上。setLargeIcon()方法 用于指定通知的大图标,当下拉系统状态栏的时候,就可以看到设置的大图标了。

以上工作完成之后,只需要调用NotificationManager的noitfy()方法 就可以上通知显示出来了。notify()方法接收两个参数:第一个参数是id,要保证该id都是唯一的;第二个参数是Notification对象。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   xmlns:android="http://schemas.android.com/apk/res/android">
   <Button
       android:onClick="setTz"
       android:layout_marginBottom="10dp"
       android:textColor="#FFFFFFFF"
       android:background="#FF0000FF"
       android:text="发送通知"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

   <Button
       android:onClick="OuttZ"
       android:textColor="#FFFFFFFF"
       android:background="#FF0000FF"
       android:text="取消通知"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
</LinearLayout>

MainActivity.java

package com.ziyia.notification;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {
   private NotificationManager manager;
   private Notification tz1;

   @Override
   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       // 通知
       // 获取通知管理器
       manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
           NotificationChannel channel = new NotificationChannel(
                   "nsyw", "测试通知", NotificationManager.IMPORTANCE_HIGH
          );
           manager.createNotificationChannel(channel);
      }

       tz1 = new NotificationCompat.Builder(this, "nsyw")
               // 设置通知标题
              .setContentTitle("我是标题")
               // 设置通知内容
              .setContentText("年少有为")
               // 添加小图标
              .setSmallIcon(R.drawable.ic_baseline_close_24)
              .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.image))
               // 设置小图标的颜色
              .setColor(Color.parseColor("#FF0000"))
               // 设置点击通知后清除通知
              .setAutoCancel(true)
//                 .setWhen(10000)
              .build();

  }

   public void setTz(View view) throws InterruptedException {
       for (int i = 0; i < 1; i++) {
           Thread.sleep(500);
           manager.notify(i, tz1);
      }
//       manager.notify(1, tz1);
  }

   public void OuttZ(View view) {
       manager.cancel(1);
  }

可以看到,我们首选获取了NotificationManager的实例,并创建了一个ID为nsyw通知东道,创建通知渠道的代码只在第一次执行的时候才会创建,当下次再执行创建代码时,系统会检测到该通知渠道已经存在了,因此不会重复创建,也并不会影响支持效率。

接下来 “发送通知“ 按钮的点击事件里完成了通知的创建工作,创建的过程正如前面所描述的一样,注意,在NotificationCompat.Builder的构造函数中传入的渠道ID也必须叫nsyw,如果传入了一个不存在的渠道ID,通知是无法显示出来的。另外,通知上显示的图标你可以使用自己准备的图片,也可以使用自定义的图片,新建一个drawable-xxhdpi目录,将图片放入即可。

如图,这是我们创建的通知渠道

通知的小图标,这是我们自定义的

通告的详细信息,可见,都是我们定义的

如何你用过安卓手机,此时应该会下意识地认为这条通知是可以点击的,但是当你去点击它时候,会发现没有任何效果。其实要想实现通知的点击效果,我们还需要在代码中进行相应的设置,这就涉及了一个新概念 PendingIntent。

PendingIntent从名字上乍越来就和Intent类似,它们确实存在不少共同点,比如它们教师可以指明一个 “意图:都 可以用于启动Activity,Service以及改善广播等,不同的是,Intent人民币于立即执行某个动作,而PendingIntent人民币于在某个合适的时机执行某个动作,所以也可以把PendingIntent简单地理解为延迟的Intent。

PendingIntent的用法同样很简单,它主要提供了几个静态方法 用于获取PendingIntent的实例。可以根据需求来选择是使用getActivity()方法 ,getBroadcast()方法,还是getService()方法。这几个方法所接收的参数都是相同的:第一个参数是Context;第二个参数一般用不到,传入0即可;第三个参数是一个Intent对象,我们可以通过这个对象构建由PendingIntent的 “意图”;第四个参数用于确定PenddingIntent的行为,有FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT这4种值 可选,通常传入0即可。

NotificationCompat.Builder这个构造器还可以连缀一个setContentIntent()方法,接收的参数正是一个PendingIntent对象,因此,这里就可以通过PendingIntent构建一个延迟的意图,当用户通知的时候,就会执行相应的逻辑。

我们来修改上方的例子,为通知加上点击跳转功能

创建一个Activity并在AndroidManfest.xml中注册,布局文件不是必须的

当我们进入该Activity时,就会弹出我们定义的内容了。

NotificationActivity.java

package com.ziyia.notification;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class NotificationActivity extends Activity {
   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       Toast.makeText(this, "我猜你是从通知跳转过来的吧", Toast.LENGTH_SHORT).show();
  }
}

在AndroidManfest.xml中注册,如图

为通知加上跳转意图,我们点击通知后跳转。

MainActivity.java

package com.ziyia.notification;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {
   private NotificationManager manager;
   private Notification tz1;

   @Override
   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       // 通知
       // 获取通知管理器
       manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
           NotificationChannel channel = new NotificationChannel(
                   "nsyw", "测试通知", NotificationManager.IMPORTANCE_HIGH
          );
           manager.createNotificationChannel(channel);
      }

       Intent intent = new Intent(this, NotificationActivity.class);
       PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

       tz1 = new NotificationCompat.Builder(this, "nsyw")
               // 设置通知标题
              .setContentTitle("我是标题")
               // 设置通知内容
              .setContentText("年少有为")
               // 添加小图标
              .setSmallIcon(R.drawable.ic_baseline_close_24)
              .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.image))
               // 设置小图标的颜色
              .setColor(Color.parseColor("#FF0000"))
               // 设置点击通知后的意图
              .setContentIntent(pendingIntent)
               // 设置点击通知后清除通知
              .setAutoCancel(true)
//                 .setWhen(10000)
              .build();

  }

   public void setTz(View view) throws InterruptedException {
       for (int i = 0; i < 1; i++) {
           Thread.sleep(500);
           manager.notify(i, tz1);
      }
//       manager.notify(1, tz1);
  }

   public void OuttZ(View view) {
       manager.cancel(1);
  }
}

可见,点击通知后跳转到了NotificationActivity并弹出了我们定义的内容。

另外,如果你够细心,你会发现,点击后通知怎么没有消失呢?是这样的,如果我们没有给通知声明取消,它会一直在状态栏上,解决的方法有两种:一种是在NotificationCompat.Builder中再连缀一个setAutoConcel()方法 ;一种显式地调用NotificationManager的cancel()方法将它取消。

所以我们可以这样写:

.setAutoCancel(true)

也可以在NotificationActivity中获取NotificationManager并调用cancel()传入通知id进行取消。

9.2.3 探究通知技巧

先来看一下NotificationCompat.Builder中的setStyle()方法,这个方法 允许我们构建出冨文本内容。也就是说,通知中不光可以有文字和图标,还可以包含更多的东西。setStyle()方法 接收一个NotificationCompat.Style参数,这个参数就是用来构建具体的冨文本信息的。如长文字,图片等。

如果通知内容太长,是无法完整显示的,多余的部分会用省略号代替。我们详细内容放到点击后打开的Activity当中会更加合适。

如果你真的非常需要在通知中显示一长段文字,Android也是支持的,通过setStyle()方法就可以做到,具体写法如下

这里使用了setStyle()方法替代setContentText()方法,在setStyle()方法 中,我们创建一个NotificationCompatlBigTextStyle对象,这个对象就是用于封装长文本信息的,只需要调用它的bigText()方法 并将文本内容传入即可。

除了显示长文本,还可以显示一张图片,具体用法是相似的,如

准备图片

添加到通知

可见,这里仍然是调用的setStyle()方法,这次我们在参数中创建了一个NotificationCompat.BigPictureStyle对象,这个对象用于设置大图片的,然后调用它的bigPicture()方法将图片传入。这里我们准备了一张图片,通过BinmapFactory的decodeResource()方法将图片解析成Bitmap对象,再传入bigPicture()方法中就可以了,效果如下

9.3 调用摄像头和相册

我们平时在QQ或者微信的时候经常需要和别人分享图片,这些图片可以是用手机摄像头拍摄的,也可以是从相册中选取的。这样的功能实在是太常见了,几乎是应用程序必备的功能。

9.3.1 调用摄像头拍照

新建一个项目

activity_main.xml

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

   <Button
       android:id="@+id/takePhotoBin"
       android:text="调用相机拍照"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
   <ImageView
       android:id="@+id/imageView"
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>
</LinearLayout>

布局中只有两个控件:一个Button和一个ImageView,Button用于打开摄像头进行拍照,而ImageView则是用于将拍到的图片显示出来。

MainActivity.java

package com.ziyia.cameraalbumtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;

import android.app.Activity;
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 android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MainActivity extends AppCompatActivity {
   private static final int TAKE_PHOTO = 1;
   private Uri imageUri;
   private File outputImage;
   private Button takePhotoBtn, fromAlbumBtn;
   private ImageView imageView;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       init();

       takePhotoBtn.setOnClickListener(new View.OnClickListener() {
           public void onClick(View v) {
// 创建File对象,用于存储拍照后的图片
               outputImage = new File(getExternalCacheDir(), "tempImage.jpg");
               if (outputImage.exists()) {
                   outputImage.delete();
              }
               try {
                   outputImage.createNewFile();
              } catch (IOException e) {
                   e.printStackTrace();
              }

               if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                   imageUri = FileProvider.getUriForFile(MainActivity.this, "com.ziyia.cameraalbumtest.FileProvider", outputImage);
              } else {
                   imageUri = Uri.fromFile(outputImage);
              }
               Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
               intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
               startActivityForResult(intent, TAKE_PHOTO); // 启动相机程序
          }
      });
  }

   private void init() {
      takePhotoBtn = findViewById(R.id.takePhotoBin);
      fromAlbumBtn = findViewById(R.id.fromAlbumBtn);
      imageView = findViewById(R.id.imageView);
  }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
       super.onActivityResult(requestCode, resultCode, data);
       switch (requestCode) {
           case TAKE_PHOTO:
               if (resultCode == Activity.RESULT_OK) {
                   try {
                       Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                       imageView.setImageBitmap(rotateIfRequired(bitmap)); // 将裁剪后的照片显示出来
                  } catch (FileNotFoundException e) {
                       e.printStackTrace();
                  }
              }
               break;
           default:
               break;
      }
  }

   private Bitmap rotateIfRequired(Bitmap bitmap) {
       Bitmap bp = null;
       try {
           ExifInterface exifInterface = new ExifInterface(outputImage.getPath());
           int attributeInt = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
           switch (attributeInt) {
               case ExifInterface.ORIENTATION_ROTATE_90:
                   bp = rotateBitmap(bitmap, 90);
               break;
               case ExifInterface.ORIENTATION_ROTATE_180:
                   bp = rotateBitmap(bitmap, 100);
                   break;
               case ExifInterface.ORIENTATION_ROTATE_270:
                   bp = rotateBitmap(bitmap, 270);
                   break;
          }
      }
       catch (IOException e) {
           e.printStackTrace();
      }

       return bp == null ? bitmap : bp;
  }

   private Bitmap rotateBitmap(Bitmap bitmap, Integer i) {
       Matrix matrix = new Matrix();
       matrix.postRotate(Float.valueOf(i));
       Bitmap bitmap1 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
       bitmap.recycle();
       return bitmap1;
  }


}

首先我们在init()方法中获取将要使用到的控件,然后给Button注册点击事件并处理调用摄像头的逻辑。

我们先创建了一个File对象,用于存放摄像头拍下的图片,这里我们把图片命名为output_image.jpg,并存放在手机SD卡应用的关联缓存目录下,什么叫作应用关联缓存目录呢?就是指SD卡中专门用于存放当前应用缓存数据的位置,调用getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data/<package name>/cache。那么为什么要使用应用关联缓存目录下存放图片呢?因为从Android6.0系统开始,读写SD卡被列为了危险权限,如果将图片存放在SD任何其他目录,都要进行运行时权限处理才行,则使用应用关联目录则可以路过这一步。另外,从Android10.0开始,公有的SD卡目录已经不再允许被应用程序直接yy8kb,而是要使用作用域存储才行。

接着会进行一个判断,如果运行设备的系统版本低于Android7.0,就调用Uri的fromFile()方法将File对象转换成Uri对象,这个Uri对象标识着output_image.jpg这张图片的本地真实路径。否则,就调用FileProvider的getUriForFile()方法 将File对象转换成一个封装过的Uri对象。getUriForFile()方法接收3个参数:第一个参数要求Context对象;第二个参数可以是任意唯一和字符串;第三个参数则是我们刚刚创建的File对象。

之所以要进行这样一层转换, 是因为从Android7.0系统开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出一个FileUnExposedException异常。而FileProvider则是一种特殊的ContentProvider,它使用了和ContentProvider类似的机制来对数据进行保护,可以选择性地将封装过的Uri共享给外部从而提高了应用的安全性。

接下来构建了一个Intent对象,并将这个Intent的action指定为android.media.action.IMGE_CAPTURE。再调用Intent的putExtra()方法指定输出地址,这里传入了刚才的Uri对象,最后调用startActivityForResult()启动 Activity。由于我们使用的是一个隐式Intent,系统会找出能够响应这个Intent的Activity去启动,这样照相机程序就会被打开,拍下的图片将会输出到output_image.jpg中

由于我们调用startActivityForResult()启动的Activity的,因此拍完照后会有结果返回到onActivityResult()方法中。如果发现拍照成功,就可以调用BitmapFactory的decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后把它调到到ImageView当中展示。

不过到这还没有结束,刚才提到了ContentProvider,那么我们就要在清单文件中对它进行注册才行

android:name的属性值是固定的,而android:authorities属性的值必须和刚才FileProvider.getUriForFile()方法第二个参数一致。另外,这里还在<provider>标题的内部使用<mata-data>指定Uri的共享路径。并引用了一个@xml/file_paths.xml资源,这个资源肯定是不存在的,我们需要手动在res目录下创建xml文件夹并在文件夹内创建file_paths.xml文件并写入以下内容

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

9.3.2 从相册中选择图片

我们还是上面项目的基础上进行修改

activity_main.xml

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

   <Button
       android:id="@+id/takePhotoBin"
       android:text="调用相机拍照"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

   <Button
       android:id="@+id/fromAlbumBtn"
       android:text="从相册中选择图片"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
   <ImageView
       android:id="@+id/imageView"
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>
</LinearLayout>

添加一个Button用于打开相册选择图片

MainActivity.java

package com.ziyia.cameraalbumtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;

import android.app.Activity;
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 android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MainActivity extends AppCompatActivity {
   private static final int TAKE_PHOTO = 1;
   private static final int FROMAIBUM = 2;
   private Uri imageUri;
   private File outputImage;
   private Button takePhotoBtn, fromAlbumBtn;
   private ImageView imageView;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       init();

       takePhotoBtn.setOnClickListener(new View.OnClickListener() {
           public void onClick(View v) {
// 创建File对象,用于存储拍照后的图片
               outputImage = new File(getExternalCacheDir(), "tempImage.jpg");
               if (outputImage.exists()) {
                   outputImage.delete();
              }
               try {
                   outputImage.createNewFile();
              } catch (IOException e) {
                   e.printStackTrace();
              }

               if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                   imageUri = FileProvider.getUriForFile(MainActivity.this, "com.ziyia.cameraalbumtest.FileProvider", outputImage);
              } else {
                   imageUri = Uri.fromFile(outputImage);
              }
               Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
               intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
               startActivityForResult(intent, TAKE_PHOTO); // 启动相机程序
          }
      });

       fromAlbumBtn.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {

               Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
               intent.addCategory(Intent.CATEGORY_OPENABLE);
               // 指定只显示图片
               intent.setType("image/*");
               startActivityForResult(intent, FROMAIBUM);
          }
      });
  }

   private void init() {

      takePhotoBtn = findViewById(R.id.takePhotoBin);
      fromAlbumBtn = findViewById(R.id.fromAlbumBtn);
      imageView = findViewById(R.id.imageView);
  }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
       super.onActivityResult(requestCode, resultCode, data);
       switch (requestCode) {
           case TAKE_PHOTO:
               if (resultCode == Activity.RESULT_OK) {
                   try {
                       Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                       imageView.setImageBitmap(rotateIfRequired(bitmap)); // 将裁剪后的照片显示出来
                  } catch (FileNotFoundException e) {
                       e.printStackTrace();
                  }
              }
               break;
           case FROMAIBUM:

               if (resultCode == Activity.RESULT_OK && data != null) {
                   Uri data1 = data.getData();
                   try {
                       Bitmap bp = BitmapFactory.decodeStream(getContentResolver().openInputStream(data1));
                       imageView.setImageBitmap(bp);
                  } catch (FileNotFoundException e) {
                       e.printStackTrace();
                  }
              }
               break;
           default:
               break;
      }
  }

   private Bitmap rotateIfRequired(Bitmap bitmap) {
       Bitmap bp = null;
       try {
           ExifInterface exifInterface = new ExifInterface(outputImage.getPath());
           int attributeInt = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
           switch (attributeInt) {
               case ExifInterface.ORIENTATION_ROTATE_90:
                   bp = rotateBitmap(bitmap, 90);
               break;
               case ExifInterface.ORIENTATION_ROTATE_180:
                   bp = rotateBitmap(bitmap, 100);
                   break;
               case ExifInterface.ORIENTATION_ROTATE_270:
                   bp = rotateBitmap(bitmap, 270);
                   break;
          }
      }
       catch (IOException e) {
           e.printStackTrace();
      }

       return bp == null ? bitmap : bp;
  }

   private Bitmap rotateBitmap(Bitmap bitmap, Integer i) {
       Matrix matrix = new Matrix();
       matrix.postRotate(Float.valueOf(i));
       Bitmap bitmap1 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
       bitmap.recycle();
       return bitmap1;
  }


}

可见,我们在新添加的Button按钮的点击事件里,我们先创建了一个Intent对象,并将它action指定为Intent.ACTION_OPEN_DOCUMENT,表示打开系统的文件选择器,。接着给这个Intent对象设置一些条件过滤,只允许可撕开的图片文件显赫出来。然后调用startActivityForResult()方法即可。注意,在调用startActivityForResult()方法的时候,我们给第二个参数传入的值 变成了FROMAIBUM,这样当选择完图片回到onActivityResult()方法时,就会进入FROMAIBUM的条件下处理图片。

接下来的部分就很简单了,我们调用了返回Intent的getData()方法 来获取选中图片的Uri,然后调用getBitmapFromUri()方法 将Uri转换成Bitmap对象,最终将图片显示到页面上。

9.4 播放多媒体文件

Android在播放音频和视频方面做了相当不错的支持,它提供了一套较为完整的API,使得开发者可以很轻松地编写出一个简易的音乐或视频播放器。

9.4.1 播放音频

在Android播放音频文件一般是使用MediaPlayer类实现的,它对多种格式的音频文件提供了非常全面的控制方法,从而使播放音乐的工作变得十分简单。

方法名功能描述
setDataSource()设置要播放的音频文件的位置
prepare()在开始播放之前调用,以完成准备工作
start()开始或继续播放音频
pause()暂停播放音频
reset()将MediaPlayer对象重置到刚刚创建的状态
seekTo()从指定的位置开始播放音频
stop()停止播放音频,调用后的MediaPlayer对象无法再播放音频
release()释放与MediaPlayer对象相关的数据
isPlaying()判断当前MediaPlayer是否正在播放
getDuration()获取载入的音频文件的时长

我们来梳理一下MediaPlayer的工作流程。首先需要创建一个MediaPlayer对象,然后调用setDataSource()方法设置音频文件的路径,再调用prepare()方法使MediaPlayer进入准备状态,接下来调用start()方法就可以开始播放音频,调用pause()方法就会暂停播放,调用reset()方法就会停止播放。

我们新建assets文件夹并将音频文件放入此文件夹

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

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

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

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

MainActivity.java

package com.ziyia.playaudiotest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {

   private MediaPlayer mediaPlayer = new MediaPlayer();
   private Button play, pause, stop;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       initMediaPlayer();
       play = findViewById(R.id.play);
       pause = findViewById(R.id.pause);
       stop = findViewById(R.id.stop);


       play.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               if (!mediaPlayer.isPlaying()) {
                   mediaPlayer.start();
              }
          }
      });

       pause.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               if (mediaPlayer.isPlaying()) {
                   mediaPlayer.pause();
              }
          }
      });

       stop.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               if (mediaPlayer.isPlaying()) {
                   mediaPlayer.reset();
                   initMediaPlayer();
              }
          }
      });
  }

   private void initMediaPlayer() {
       AssetManager assets = getAssets();
       try {
           AssetFileDescriptor assetFileDescriptor = assets.openFd("music.mp3");
           mediaPlayer.setDataSource(assetFileDescriptor.getFileDescriptor(), assetFileDescriptor.getStartOffset(), assetFileDescriptor.getLength());
           mediaPlayer.prepare();
      } catch (IOException e) {
           e.printStackTrace();
      }
  }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       mediaPlayer.stop();
       mediaPlayer.release();
  }
}

可见,在类初始化的时候,我们就先创建了一个MediaPlayer的实例,然后在orCreate()方法 中调用initMediaPlayer()方法,为MediaPlayer对象进行初始化操作,在initMedPlayer()方法中,首先通过getAssets()方法得到了AssetManager的实例,AssetsManager可用于读取assets上下的任何资源,接着我们调用了openFd()方法 将音频文件句柄打开,后面又依次调用了setDataSource()方法和prepare()方法,为MediaPlayer做好了播放前的准备。

接下来我们看一下 各个按钮的点击中的代码。当点击 “Play” 按钮时会进行判断,如果当前MediaPlayer没有正在播放音频,则调用start()方法开始播放。当点击”Pause”按钮时会判断,如果当前MediaPlayer正在播放音频,则调用pause()方法暂停播放。当点击 “Stop”按钮时会判断,如果当前MediaPlayer正在播放音频,则调用reset()方法将MediaPlayer重置为刚刚创建的状态,然后重新调用一次initMediaPlayer()方法。

运行应用程序查看效果吧,嘿嘿。

9.4.2 播放视频

播放视频文件其实不比播放音频文件复杂,主要是VideoView类来实现的,这个类将视频的显示和控制集于一身,我们仅仅借助它就可以完成一个简单的视频播放器。

方法名功能描述
setVideoPath()设置要播放的视频文件的位置
start()开始或继续播放视频
pause()暂停播放视频
resume()将视频从头开始播放
seekTo()从指定的位置开始播放视频
isPlaying()判断当前是否正在播放视频
getDuration()获取载入的视频文件的时长

具体使用方法如下:

activity_main.xml

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

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

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

   <Button
       android:id="@+id/replay"
       android:text="Replay"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

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

这个布局文件中同样旋转了3个按钮,分别用于控制视频的播放,眾和重新播放。另外挖掘的下面又放置了一个VideoView。

接下来就是放置视频资源文件了,很可惜的是,VideoView不支持播放assets目录下的视频资源。res允许秩再创建一个raw目录,像音频,视频资源也可以放在这里,并且VideoView是可以直接播放这个目录下的视频资源的。

MainActivity.java

package com.ziyia.playvideotest;

import androidx.appcompat.app.AppCompatActivity;

import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.VideoView;

public class MainActivity extends AppCompatActivity {
   private VideoView videoView;
   private Button play, pause, replay;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       init();
       Uri uri = Uri.parse("android.resource://"+getPackageName()+"/"+ R.raw.video);
       videoView.setVideoURI(uri);

       play.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               if (!videoView.isPlaying()) {
                   videoView.start();
              }
          }
      });

       pause.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               if (videoView.isPlaying()) {
                   videoView.pause();
              }
          }
      });

       replay.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               if (videoView.isPlaying()) {
                   videoView.resume();
              }
          }
      });
  }


   private void init() {
       videoView = findViewById(R.id.videoView);
       play = findViewById(R.id.play);
       pause = findViewById(R.id.pause);
       replay = findViewById(R.id.replay);
  }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       videoView.suspend();
  }
}

上方代码就非常简单了,因为它和前面播放音频的代码非常相似,我们首先在onCreate()方法中调用了Uri.parse()方法 ,将raw上下录的video.mp4文件解析成了一个Uri对象,这里使用的是Android要求的固定写法,然后调用VideoView的setVideoPath()方法将刚才解析的Uri对象传入,这样VideoView便初始化完成了。

最后在onDestroy()方法中,我们还需要调用suspend()方法,将VideoView所占用的资源释放掉。

编译运行程序,点击Play,视频便开始播放了。

发表回复

相关

浙ICP备2021031744号-3