第一行代碼學習筆記第八章——運用手機多媒體

知識點目錄

知識點回顧

8.1 將程序運行到手機上

前面的章節我們都是使用模擬器來運行程序的,但這章的Demo都需要在真正的Android手機上運行。

打開開發者選項:

從Android4.2系統開始,開發者選項是默認隱藏的,需要先進入到“關於手機”界面,然後對連續點擊版本號,直到讓開發者選項顯示出來。

手機連接電腦

打開開發者選項後,我們還需要通過數據線把手機連接到電腦上。然後進入到設置—>開發者選項界面,並在這個界面中勾選中USB調試選項。

如果是Windows操作系統,還需要在電腦上安裝手機驅動。一般藉助360手機助手或豌豆莢等工具進行快速安裝,安裝完成後就可以看到手機已經連接到電腦上了:

8.2 使用通知

通知的主要的作用是:當應用程序不是在前臺運行時,向用戶發出一些提示信息。

發出一條通知後,手機最上方的狀態欄會顯示一個通知的圖標,下拉狀態欄後可以看到通知的詳細內容。

8.2.1 通知的基本使用

通知可以在活動、廣播接收器或服務裏創建。但無論在哪創建使用步驟都是一樣的。

  1. 獲取NotificationManager來對通知進行管理

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

  2. 創建Notification對象

     Notification notification = new NotificationCompat.Builder(this)
             .setContentTitle("This is content title")
             .setContentText("This is content text")
             .setWhen(System.currentTimeMillis())
             .setSmallIcon(R.mipmap.ic_launcher)
             .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
             .build();
    

幾乎每個Android版本都會對通知這部分進行修改,爲了更好的兼容性,我們使用support庫中提供的兼容API。support-v4庫中提供了一個NotificationCompat類,使用這個類的構造器來創建Notification對象,就可以保證我們的程序在所有Android系統版本上都能正常工作。

  • setContentTitle()方法指定通知的標題內容

  • setContentText()方法指定通知的正文內容

  • setWhen()方法指定通知被創建的時間,以毫秒爲單位,當下拉系統狀態欄時,這裏指定的時間會顯示在相應的通知上

  • setSmallIcon()方法設置通知的小圖標,只能使用純alpha圖層的圖片進行設置,小圖標會顯示在系統狀態欄上

  • setLargeIcon()方法設置通知的大圖標,下拉系統狀態欄時,就可以看到設置的大圖標

  1. 調用NotificationManager的notify()方法

     manager.notify(1,notification);
    

    參數一:id,要保證爲每個通知所指定的id都是不同的

    參數二:創建的Notification對象

示例代碼:

  1. 定義一個notification觸發點

     <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
     
         <Button
             android:id="@+id/send_notice"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textAllCaps="false"
             android:text="Send notice"/>
     
     </LinearLayout>
    
  2. 創建通知

     public class MainActivity extends AppCompatActivity implements View.OnClickListener {
     
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
     
             Button sendNotice = (Button) findViewById(R.id.send_notice);
             sendNotice.setOnClickListener(this);
         }
     
         @Override
         public void onClick(View v) {
             switch (v.getId()) {
                 case R.id.send_notice:
                     String id = "my_channel_01";
                     String name = "我是渠道名字";
                     NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
                     Notification notification = null;
                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//Android 8.0以上
                         Toast.makeText(this, "Build.VERSION.SDK_INT = " + Build.VERSION.SDK_INT, Toast.LENGTH_SHORT).show();
                         NotificationChannel channel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT);
                         manager.createNotificationChannel(channel);
                         notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
                                 .setContentTitle("This is content title")
                                 .setContentText("heheda")
                                 .setWhen(System.currentTimeMillis())
                                 .setSmallIcon(R.mipmap.ic_launcher)
                                 .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                                 .build();
                     } else {//Android8.0以下
                         Toast.makeText(this, "Build.VERSION.SDK_INT = " + Build.VERSION.SDK_INT, Toast.LENGTH_SHORT).show();
                         notification = new NotificationCompat.Builder(this)
                                 .setContentTitle("This is content title")
                                 .setContentText("hahaha")
                                 .setWhen(System.currentTimeMillis())
                                 .setSmallIcon(R.mipmap.ic_launcher)
                                 .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                                 .build();
                     }
                     manager.notify(1, notification);
                     break;
                 default:
                     break;
             }
         }
     }
    

備註:在Android8.0以上發送通知的稍有不同,需要NotificationChannel。

PendingIntent

PendingIntent跟Intent有許多相似之處,都可以指明一個"意圖",都可以啓動活動、啓動服務以及發送廣播。不同的是Intent更加傾向於去立即執行某個動作,而PendingIntent更加傾向於在某個合適的時機去執行某個動作。所以,可以把PendingIntent理解爲延遲執行的Intent

PendingIntent的基本用法:

  1. 獲取PendingIntent實例

根據具體的需求來選擇使用getActivity()方法、getBroadcast()方法或getService()方法。

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

getActivity()方法、getBroadcast()方法或getService()方法接收的參數都是相同的:

參數一:上下文

參數二:請求碼,一般傳入0即可

參數三:Intent對象

參數四:用於確定PendingIntent的行爲。有FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT這4個值可選,通常傳入0即可。

2.連綴在setContentIntent()方法中

notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
        .setContentTitle("This is content title")
        .setContentText("heheda")
        .setWhen(System.currentTimeMillis())
        .setSmallIcon(R.mipmap.ic_launcher)
        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
        .setContentIntent(pendingIntent) //連綴PendingIntent
        .build();

這樣下拉系統狀態欄,點擊通知後,就會進入到NotificationActivity界面。

讓狀態欄上通知欄消失:

上面進入到NotificationActivity界面後,發現系統狀態欄上的通知欄圖標還沒有消失。

讓狀態欄上通知欄消失的方法有兩種:

  • 在NotificationCompat.Builder中再連綴一個

      notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
              .setContentTitle("This is content title")
              .setContentText("heheda")
              .setWhen(System.currentTimeMillis())
              .setSmallIcon(R.mipmap.ic_launcher)
              .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
              .setContentIntent(pendingIntent)
              .setAutoCancel(true) //點擊這個通知時,通知會自動取消掉
              .build();
    
  • 顯示地調用NotificationManager的cancel()方法

      public class NotificationActivity extends AppCompatActivity {
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_notification);
              NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
              manager.cancel(1);//這裏的1,就是創建通知的時候給每條通知指定的id
          }
      }
    
8.2.2 通知的進階技巧

NotificationCompat.Builder中提供了非常豐富的API來讓我們創建出更加多樣的通知效果。下面我們學習一些常用的API。

setSound()

在通知發出的時候播放一段音頻。setSound()方法接收一個Uri參數,所以在指定音頻文件的時候還需要先獲取到音頻文件對應的URI。例如:每個手機的/system/media/audio/ringtones目錄下很多音頻文件,我們可以從中隨便選一個音頻文件,那麼就可以在代碼中這樣指定;

Notification notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
        ....
        .setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg"))) //指定音頻
        .build();

setVibrate()

用於設置手機靜止和振動的時長,以毫秒爲單位。下標爲0的值表示手機靜止的時長,下標爲1的值表示手機振動的時長,下標爲2的值又表示手機靜止的時長,以此類推。

Notification notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
            ....
            .setVibrate(new long[]{0,1000,1000,1000}) //指定振動時長
            .build();

備註:控制手機振動需要聲明權限:

<uses-permission android:name="android.permission.VIBRATE"/>

setLights()

用於控制在通知到來時,控制手機LED燈的顯示。

Notification notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
            ....
            .setLights(Color.GREEN,1000,1000)
            .build();

參數一:指定LED燈的顏色

參數二:指定LED燈亮起的時長,以毫秒爲單位

參數三:指定LED燈暗去的時長,以毫秒爲單位

setDefaults()

如果不想繁雜的設置,可以直接使用通知的默認效果,它會根據當前手機的環境來決定播放什麼鈴聲,以及如何振動。

Notification notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
            ....
            .setDefaults(NotificationCompat.DEFAULT_ALL) //使用默認效果
            .build();
8.2.3 通知的高級功能

NotificationCompat.Builder中還有更強大的API,可以構建出更加豐富的通知效果。

setStyle()

當通知欄中的內容足夠長的時候,正常情況下是無法完全顯示的,如下圖所示:

如果我們想讓通知欄中的內容完全顯示,可以使用setStyle()方法:

Notification notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
            ....
            .setStyle(new NotificationCompat.BigTextStyle().bigText("Learn how to build notification ,send and sync data,and use voice actions. Get the official" +
                                "Andoird IDE and developer tools to build apps for Android."))
            .build();

運行後的效果如下:

除了顯示長文字之外,通知裏還可以顯示一張大圖片,具體用法如下:

Notification notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
            ....
            .setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(),R.drawable.big_image)))
            .build();

運行後效果如下:

setPriority()

用於設置通知的重要程度。該方法接收一個整型參數用於設置這條通知的重要程度,共有5個指可選:

  • PRIORITY_DEFAULT 表示默認的重要程度

  • PRIORITY_MIN 表示最低的重要程度

  • PRIORITY_LOW 表示較低的重要程度

  • PRIORITY_HIGH 表示較高的重要程度

  • PRIORITY_MAX 表示最高的重要程度

具體用法如下:

Notification notification = new NotificationCompat.Builder(this, id)//這裏要傳一個id進去
            ....
            .setPriority(NotificationCompat.PRIORITY_MAX)
            .build();

8.3 調用攝像頭和相冊

調用系統的攝像頭進行拍照或者調用系統中的相冊都是很常見的功能需求。

8.3.1 調用攝像頭拍照

調用系統攝像頭拍照的步驟一般如下:

第一步:定義一個觸發點

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

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

</LinearLayout>

佈局中一個Button和一個ImageView。Button用於打開攝像頭進行拍照,ImageView用於將拍到的圖片顯示出來。

第二步:調用攝像頭的具體邏輯

public class MainActivity extends AppCompatActivity {

    private static final int TAKE_PHOTO = 1;
    private Button mTakePhoto;
    private Button mMTakePhoto;
    private ImageView mPicture;
    private Uri mImageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mMTakePhoto = (Button) findViewById(R.id.take_photo);
        mPicture = (ImageView) findViewById(R.id.picture);

        mMTakePhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //創建File對象,用於存儲拍照後的照片
                File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
                try {
                    if (outputImage.exists()) {
                        outputImage.delete();
                    }
                    outputImage.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                //將創建的File對象轉換爲Uri對象
                if (Build.VERSION.SDK_INT >= 24) {
                    mImageUri = FileProvider.getUriForFile(MainActivity.this, "com.example.cameraalbumtest.fileprovider", outputImage);
                } else {
                    mImageUri = Uri.fromFile(outputImage);
                }

                //啓動相機程序
                Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
                intent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri);
                startActivityForResult(intent,TAKE_PHOTO);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case TAKE_PHOTO:
                if (resultCode == RESULT_OK) {
                    try {
                        //將拍攝的照片顯示出來
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(mImageUri));
                        mPicture.setImageBitmap(bitmap);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }
    }
}

對上面的代碼分四大塊進行解釋:

1. 創建存放拍下圖片的路徑

創建一個File對象,用於存放攝像頭拍下的圖片,並命名爲output_image.jpg,並將它存放在手機SD卡的應用關聯緩存目錄下。

應用關聯緩存目錄是指SD卡中專門用於存放當前應用緩存數據的位置,調用getExternalCacheDir()方法就可以得到這個目錄,具體路徑是/sdcard/Android/data/com.example.cameraalbumtest/cache ,如下圖所示:

爲什麼要是使用應用關聯緩存目錄呢?因爲從Android6.0開始,讀寫SD卡被列爲危險權限,如果將圖片存放在SD卡的任何其他目錄,都需要進行運行是權限處理才行,而使用應用關聯目錄則可以逃過這一步。

2. 將創建的File對象轉換爲Uri對象

在低於Android7.0時,就調用Uri的fromFile()方法將File對象轉換爲Uri對象。

在高於Android7.0時,就調用FileProvider的getUriForFile()方法將File對象轉換成一個封裝過的Uri對象。該方法共接收三個參數:

參數一:Context對象

參數二:authority字符串。需要跟後面再AndroidManifest.xml註冊時保持一致

參數三:創建的File對象

之所以要進行這樣一層轉換,因爲從Android7.0開始,直接使用本地真實路徑的Uri被認爲是不安全的,會拋出一個FileUriExposedException異常,而FileProvider則是一種特殊的內容提供器,它使用了和內容提供器類似的機制來對數據進行了保護,可以選擇性地將封裝過的Uri共享給外部,從而提高了應用的安全性。

這個Uri對象標識着output_image.jpg這張圖片的本地真實路徑。

3. 啓動相機程序

構建一個Intent對象,並將Intent的action指定爲android.media.action.IMAGE_CAPTURE,再調用putExtra()方法指定圖片的輸出地址,最後調用startActivityForResult()來啓動活動。

這樣我是使用的隱式Intent,系統會找到能夠響應這個Intent的活動去啓動,這樣照相機程序就會被打開,拍下的照片將會輸出到output_image.jpg中

4. 將照片顯示出來

因爲上面用的是startActivityForResult()方法,當照相機程序拍照完成後,就會返回到onActivityResult()方法中,如果發現拍照成功,就可以調用BitmapFactory的decodeStream()方法將output_image.jpg這張照片解析成Bitmap對象,然後把它設置到ImageView中顯示出來。

第三步:註冊內容提供器

上面用到了FileProvider,所以就需要到AndroidManifest.xml中去註冊:

<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">
    <provider
        android:authorities="com.example.cameraalbumtest.fileprovider"
        android:name="android.support.v4.content.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"/>
    </provider>
</application>

屬性解釋:

  • android:authorities 指定authorities,需要與FileProvider.getUriForFile()方法中的第二個參數一致

  • android:name 固定值。必須是android.support.v4.content.FileProvider

  • android:exported 表示是否允許外部程序訪問我們的內容提供器

  • android:grantUriPermissions 用於給內容提供器的數據子集授權。true表示權限能夠被授予內容提供器範圍內的任何數據;false表示權限只能授予這個元素所指定的數據子集。

  • 指定Uri的共享路徑,並應用一個@xml/file_paths資源

第四步:創建file_paths資源

右擊res目錄—>New—>Directory,創建一個xml目錄,接着右擊xml目錄—>New—>File,創建一個file_paths.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>

其中:external-path就是用來指定Uri共享的,name屬性的值可以隨便填寫,path屬性的值表示共享的具體路徑。這裏的.就表示將整個SD卡進行共享,當然也可以僅共享我們存放output_image.jpg這張圖片的路徑。

第五步:添加權限

在Android4.4之前,訪問SD卡的應用關聯目錄也是要權限的,Android4.4之後的系統則不再需要權限聲明瞭,爲了能夠兼容老版本,在AndroidManifest.xml中加入相應權限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

效果圖:

將程序運行到手機上,點擊Take Photo按鈕進行拍照,拍照完成後,點擊√,回到我們程序界面,拍照的照片就會顯示出來。如下圖所示:

8.3.2 從相冊中選擇照片

從相冊中選擇已經拍攝好的照片也是非常常見的功能。

實現從相冊中選擇照片的功能一般步驟如下:

第一步:新增一個觸發點

在前面CameraAlbumTest的基礎上,添加一個Button:

<Button
    android:id="@+id/choose_from_album"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAllCaps="false"
    android:text="Choose From Album"/>

Button用於打開系統相冊,然後再在ImageView上將選中的圖片顯示出來。

第二步:打開相冊的具體邏輯

public class MainActivity extends AppCompatActivity {

    private static final int TAKE_PHOTO = 1;
    private static final int CHOOSE_PHOTO = 2;
    private Button mMTakePhoto;
    private ImageView mPicture;
    private Uri mImageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mMTakePhoto = (Button) findViewById(R.id.take_photo);
        mPicture = (ImageView) findViewById(R.id.picture);

        ......

        //從相冊中選擇照片
        Button chooseFromAlbum = (Button) findViewById(R.id.choose_from_album);
        chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
				//權限申請
                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
                } else {
                    openAlbum();
                }
            }
        });
    }
	
    private void openAlbum() {
		//打開相冊
        Intent intent = new Intent("android.intent.action.GET_CONTENT");
        intent.setType("image/*");
        startActivityForResult(intent, CHOOSE_PHOTO);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    openAlbum();
                } else {
                    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {

            ......

            case CHOOSE_PHOTO:
                if (resultCode == RESULT_OK) {
                    //判斷手機系統的版本號
                    if (Build.VERSION.SDK_INT >= 19) {
                        handleImageOnKitKat(data);
                    } else {
                        handleImageBeforeKitKat(data);
                    }
                }
                break;
            default:
                break;
        }
    }

    @TargetApi(19)
    private void handleImageOnKitKat(Intent data) {
        String imagePath = null;
        Uri uri = data.getData();
        if (DocumentsContract.isDocumentUri(this,uri)) {
            //如果是document類型的Uri,則通過document id處理
            String docId = DocumentsContract.getDocumentId(uri);
            if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
                String id = docId.split(":")[1];//解析出數字格式的id
                String selection = MediaStore.Images.Media._ID + "=" + id;
                imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
            } else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
                Uri cententUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),Long.valueOf(docId));
                imagePath = getImagePath(cententUri, null);
            }
        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
            //如果是content類型的Uri,則使用普通方式處理
            imagePath = getImagePath(uri, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme())) {
            //如果是file類型的Uri,直接獲取圖片路徑即可
            imagePath = uri.getPath();
        }
        displayImage(imagePath);
    }

    private void handleImageBeforeKitKat(Intent data) {
        Uri uri = data.getData();
        String imagePath = getImagePath(uri, null);
        displayImage(imagePath);
    }

    private String getImagePath(Uri uri, String selection) {
        String path = null;
        Cursor cursor = null;
        //通過Uri和selection來獲取真實的圖片路徑
        try {
            cursor = getContentResolver().query(uri, null, selection, null, null);
            if (cursor != null) {
                if (cursor.moveToFirst()) {
                    path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return path;
    }

	//將照片顯示出來
    private void displayImage(String imagePath) {
        if (imagePath != null) {
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
            mPicture.setImageBitmap(bitmap);
        } else {
            Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
        }
    }
}

解釋說明:

  • 需要動態申請WRITE_EXTERNAL_STORAGE這個危險權限

  • 從Android4.4開始,選取相冊中的圖片不再返回圖片真實的Uri了,而是一個封裝過的Uri,所以在4.4版本以上的手機需要對這個Uri進行解析。返回的Uri分爲document、content和file三種類型,需要從這三種情況去分別判斷解析。

效果圖:

重新運行程序,點擊Choose From Album按鈕,首先會彈出權限申請框,點擊允許後就會打開手機相冊,然後隨意選擇一張照片,回到我們程序的界面,選中的照片就會顯示出來,如下圖所示:

調用攝像頭拍照和從相冊中選擇照片的代碼我已上傳到我GitHub,有需要的朋友可以進入查看:

調用攝像頭拍照和從相冊中選擇照片功能實現

8.4 播放多媒體文件

Android在播放音頻和視頻方面提供了一套較爲完整的API,使得開發者可以很輕鬆地編寫出一個簡易的音頻或視頻播放器。

8.4.1 播放音頻

Android中播放音頻文件一般都是使用MediaPlayer類來實現。

MediaPlayer中常用的控制方法如下:

下面我們在項目中實踐下:

第一步:編寫佈局控件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

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

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

</LinearLayout>

佈局中放置3個按鈕,分別用於對音頻文件的播放、暫停和停止操作。

第二步:音頻功能的具體實現

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "MainActivity";
    private MediaPlayer mMediaPlayer = new MediaPlayer();

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

        play.setOnClickListener(this);
        pause.setOnClickListener(this);
        stop.setOnClickListener(this);

        //申請權限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
        } else {
            initMediaPlayer();//初始化MediaPlayer
        }
    }

    private void initMediaPlayer() {
        try {
            File file = new File(Environment.getExternalStorageDirectory(),"audio.wma");
            mMediaPlayer.setDataSource(file.getPath());//指定音頻文件的路徑
            mMediaPlayer.prepare();//讓MediaPlayer進入到準備狀態
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    initMediaPlayer();
                } else {
                    Toast.makeText(this, "拒絕權限將無法使用程序", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.play:
                if (!mMediaPlayer.isPlaying()) {
                    mMediaPlayer.start();//開始播放
                }
                break;
            case R.id.pause:
                if (mMediaPlayer.isPlaying()) {
                    mMediaPlayer.pause();//暫停播放
                }
                break;
            case R.id.stop:
                if (mMediaPlayer.isPlaying()) {
                    mMediaPlayer.stop();//停止播放
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
        }
    }
}

第三步:加入權限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

效果圖:

運行程序後,會先申請權限,同意授權後,就會出現如下界面:

點擊Play按鈕就會播放音頻。

點擊Pause按鈕,停止播放音頻,再次點擊Play按鈕,會接着暫停之前的位置繼續播放。

點擊Stop按鈕,停止播放音頻,但再次點擊Play按鈕時,音樂會從從頭開始播放。

8.4.2 播放視頻

Android中播放視頻文件一般都是使用VideoView類來實現。

VideoView中常用的控制方法如下:

下面我們在項目中實踐下:

第一步:編寫佈局控件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

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

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

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

    </LinearLayout>

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

</LinearLayout>

放置3個按鈕,分別用於控制視頻的播放、暫停和重新播放。下面放置一個VideoView,用於顯示要播放的視頻。

第二步:視頻功能的具體實現

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "MainActivity";

    private VideoView mVideoView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mVideoView = (VideoView) findViewById(R.id.video_view);
        Button play = (Button) findViewById(R.id.play);
        Button pause = (Button) findViewById(R.id.pause);
        Button replay = (Button) findViewById(R.id.replay);

        play.setOnClickListener(this);
        pause.setOnClickListener(this);
        replay.setOnClickListener(this);

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        } else {
            initVideoPath();//初始化VideoView
        }
    }

    private void initVideoPath() {
        File file = new File(Environment.getExternalStorageDirectory(), "ad.mp4");
        mVideoView.setVideoPath(file.getPath());
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.play:
                if (!mVideoView.isPlaying()) {
                    mVideoView.start(); //開始播放
                }
                break;
            case R.id.pause:
                if (mVideoView.isPlaying()) {
                    mVideoView.pause(); //暫停播放
                }
                break;
            case R.id.replay:
                if (mVideoView.isPlaying()) {
                    mVideoView.resume(); //重新播放
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    initVideoPath();
                } else {
                    Toast.makeText(this, "拒絕權限將無法使用程序", Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mVideoView != null) {
            mVideoView.suspend(); //將VideoView所佔的資源釋放掉
        }
    }
}

第三步:加入權限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

效果圖:

運行程序後,會先申請權限,同意授權後,點擊Play按鈕,就可以看到視頻已經開始播放了,如下圖所示:

點擊Pause按鈕可以暫停視頻的播放,點擊Replay按鈕可以從頭開始播放視頻。

VideoView只是幫我們做了一個很好的封裝,它的背後仍然使用MediaPlayer來對視頻文件進行控制。

VideoView並不是一個萬能的視頻播放工具類,它在視頻格式的支持以及播放效率方面都存在較大的不足。所以,如果想僅僅用VideoView編寫出一個功能非常強大的視頻播放器是不太現實的。比較適合於用於播放一些遊戲類的片頭動畫或某個應用的宣傳視頻。

8.5 小結與點評

本章主要對Android系統的各種多媒體技術進行了學習,主要有:

  • 通知的使用技巧

  • 調用攝像頭拍照

  • 從相冊中選取照片

  • 播放音頻文件

  • 播放視頻文件

非常感謝您的耐心閱讀,希望我的文章對您有幫助。歡迎點評、轉發或分享給您的朋友或技術羣。
發佈了46 篇原創文章 · 獲贊 72 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章