主頁 > 移動端開發 > 《第一行代碼:Android篇》學習筆記(八)

《第一行代碼:Android篇》學習筆記(八)

2022-05-12 08:29:59 移動端開發

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,感激感激郭霖先生提供這么好的書籍,

第8章 豐富你的程式——運用手機多媒體

本章將對Android中一些常用的多媒體功能的使用技巧進行學習,前面的7章內容,一直都是使用模擬器來運行程式的,不過本章涉及的一些功能必須要在真正的Android手機上運行才看得到效果,因此,首先我們就來學習一下,如何使用Android手機來運行程式,

8.1 將程式運行到手機上

想要將程式運行到手機上,需要先通過資料線把手機連接到電腦上,然后進入到設定→開發者選項界面,并在這個界面中勾選中USB除錯選項,如圖:

image

注意:從Android 4.2系統開始,開發者選項默認是隱藏的,你需要先進入到“關于手機”界面,然后對著最下面的版本號那一欄連續點擊,就會讓開發者選項顯示出來,

如果你使用的是Windows作業系統,還需要在電腦上安裝手機的驅動,一般借助360手機助手或豌豆莢等工具都可以快速地進行安裝,安裝完成后就可以看到手機已經連接到電腦上了,

手上沒有安卓手機,下圖為華為鴻蒙系統和華為手機助手操作流程:

image

image

image

image

繼續書上的內容:現在觀察Android Monitor,你會發現當前是有兩個設備在線的,一個是我們一直使用的模擬器,另外一個則是剛剛連接上的手機了,如圖:

image

然后,運行一下當前專案,這時不會直接將程式運行到模擬器或者手機上,而是會彈出一個對話框讓你進行選擇,如圖:

image

選中下面的LGE Nexus 5后點擊OK,就會將程式運行到手機上了,

8.2 使用通知

知(Notification)是Android系統中比較有特色的一個功能,當某個應用程式希望向用戶發出一些提示資訊,而該應用程式又不在前臺運行時,就可以借助通知來實作,發出一條通知后,手機最上方的狀態欄中會顯示一個通知的圖示,下拉狀態欄后可以看到通知的詳細內容,Android的通知功能獲得了大量用戶的認可和喜愛,就連iOS系統也在5.0版本之后加入了類似的功能,

8.2.1 通知的基本用法

通知的用法還是比較靈活的,既可以在活動里創建,也可以在廣播接收器里創建,還可以在下一章中即將學習的在服務里創建,相比于廣播接收器和服務,在活動里創建通知的場景還是比較少的,因為一般只有當程式進入到后臺的時候我們才需要使用通知,

無論是在哪里創建通知,整體的步驟都是相同的,首先需要一個NotificationManager來對通知進行管理,可以呼叫Context的getSystemService()方法獲取到,getSystemService()方法接收一個字串引數用于確定獲取系統的哪個服務,這里我們傳入Context.NOTIFICATION_SERVICE即可,因此,獲取NotificationManager的實體就可以寫成:

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

接下來,需要使用一個Builder構造器來創建Notification物件,但問題在于,幾乎Android系統的每一個版本都會對通知這部分功能進行或多或少的修改,API不穩定性問題在通知上面突顯得尤其嚴重,

那么該如何解決這個問題呢?

其實解決方案我們之前已經見過好幾回了,就是使用support庫中提供的兼容API,support-v4庫中提供了一個NotificationCompat類,使用這個類的構造器來創建Notification物件,就可以保證我們的程式在所有Android系統版本上都能正常作業了,代碼如下所示:

Notification notification = new NotificationCompat.Builder(context).build();

Android O 版本后,NotificationCompat.Builder()過時,失效,

image

書中上述方法被以下方法取代:

NotificationCompat.Builder(Context context, String channelId)

結合網上找到的解決方法:https://blog.csdn.net/qq_32534441/article/details/103501273 ,解決了上述問題,代碼如下:

NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    String channelId = "default";
                    String channelName = "默認通知";
                    //new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
                    manager.createNotificationChannel(new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH));
                }
                Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        .setContentTitle("This is content title")
                        .setContentText("This is content text")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.drawable.small_icon)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                                R.drawable.large_icon)).build();
                manager.notify(1,notification);

一共呼叫了5個設定方法:

  • setContentTitle()方法用于指定通知的標題內容,下拉系統狀態欄就可以看到這部分內容,
  • setContentText()方法用于指定通知的正文內容,同樣下拉系統狀態欄就可以看到這部分內容,
  • setWhen()方法用于指定通知被創建的時間,以毫秒為單位,當下拉系統狀態欄時,這里指定的時間會顯示在相應的通知上,
  • setSmallIcon()方法用于設定通知的小圖示,注意只能使用純alpha圖層的圖片進行設定,小圖示會顯示在系統狀態欄上,
  • setLargeIcon()方法用于設定通知的大圖示,當下拉系統狀態欄時,就可以看到設定的大圖示了,

以上作業都完成之后,只需要呼叫NotificationManager的notify()方法就可以讓通知顯示出來了,notify()方法接收兩個引數,第一個引數是id,要保證為每個通知所指定的id都是不同的,第二個引數則是Notification物件,這里直接將我們剛剛創建好的Notification物件傳入即可,因此,顯示一個通知就可以寫成:

manager.notify(1,notification);

到這里就已經把創建通知的每一個步驟都分析完了,下面就讓我們通過一個具體的例子來看一看通知到底是長什么樣的,新建一個NotificationTest專案,并修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/send_notice"
        android:text="Send notice"/>
</LinearLayout>

布局檔案非常簡單,里面只有一個Send notice按鈕,用于發出一條通知,接下來修改MainActivity中的代碼,如下所示:

package com.zhouzhou.notificationtest;

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

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

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 view) {
        switch (view.getId()) {
            case R.id.send_notice:
                NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    String channelId = "default";
                    String channelName = "默認通知";
                    //new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
                    manager.createNotificationChannel(new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH));
                }
                //small_icon和large_icon是自己在網上找的圖片
                Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        .setContentTitle("My notification")
                        .setContentText("Hello World!")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.drawable.small_icon)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                                R.drawable.large_icon)).build();
                manager.notify(1,notification);
                break;
            default:
                break;
        }
    }
}

可以看到,我們在Send notice按鈕的點擊事件里面完成了通知的創建作業,現在可以來運行一下程式了,點擊Send notice按鈕,會在系統狀態欄看到圖示,如圖:

image

下拉系統狀態欄可以看到該通知的詳細資訊,如圖:

image

當你下拉系統狀態欄并點擊這條通知的時候,會發現沒有任何效果,

要想實作通知的點擊效果,還需要在代碼中進行相應的設定,這就涉及了一個新的概念:PendingIntent,

PendingIntent從名字上看起來就和Intent有些類似,它們之間也確實存在著不少共同點,比如它們都可以去指明某一個“意圖”,都可以用于啟動活動、啟動服務以及發送廣播等,不同的是,Intent更加傾向于去立即執行某個動作,而PendingIntent更加傾向于在某個合適的時機去執行某個動作,所以,也可以把PendingIntent簡單地理解為延遲執行的Intent,

PendingIntent的用法同樣很簡單,它主要提供了幾個靜態方法用于獲取PendingIntent的實體,可以根據需求來選擇是使用getActivity()方法、getBroadcast()方法,還是getService()方法,這幾個方法所接收的引數都是相同的:

  • 第一個引數依舊是Context,不用多做解釋,
  • 第二個引數一般用不到,通常都是傳入0即可,
  • 第三個引數是一個Intent物件,我們可以通過這個物件構建出PendingIntent的“意圖”,
  • 第四個引數用于確定PendingIntent的行為,有FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT這4種值可選,每種值的具體含義你可以查看檔案,通常情況下這個引數傳入0就可以了,

對PendingIntent有了一定的了解后,我們再回過頭來看一下NotificationCompat.Builder,這個構造器還可以再連綴一個setContentIntent()方法,接收的引數正是一個PendingIntent物件,因此,這里就可以通過PendingIntent構建出一個延遲執行的“意圖”,當用戶點擊這條通知時就會執行相應的邏輯,

現在我們來優化一下NotificationTest專案,給剛才的通知加上點擊功能,讓用戶點擊它的時候可以啟動另一個活動,首先需要準備好另一個活動,右擊com.zhouzhou.notificationtest包→New→Activity→Empty Activity,新建NotificationActivity,布局起名為notification_layout,然后修改notification_layout.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_height="match_parent">
    <TextView
        android:layout_
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="24sp"
        android:text="This is notification layout"/>
</RelativeLayout>

這樣就把NotificationActivity這個活動準備好了,下面我們修改MainActivity中的代碼,給通知加入點擊功能,如下所示:

package com.zhouzhou.notificationtest;

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.Intent;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

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 view) {
        switch (view.getId()) {
            case R.id.send_notice:
                Intent intent = new Intent(this,NotificationActivity.class);
                PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,0);
                NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    String channelId = "default";
                    String channelName = "默認通知";
                    //new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
                    manager.createNotificationChannel(new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH));
                }
                //small_icon和large_icon是自己在網上找的圖片
                Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        .setContentTitle("My notification")
                        .setContentText("Hello World!")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.drawable.small_icon)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.large_icon))
                        .setContentIntent(pendingIntent)
                        .build();
                manager.notify(1,notification);
                break;
            default:
                break;
        }
    }
}

可以看到,這里先是使用Intent表達出我們想要啟動NotificationActivity的“意圖”,然后將構建好的Intent物件傳入到PendingIntent的getActivity()方法里,以得到PendingIntent的實體,接著在NotificationCompat.Builder中呼叫setContentIntent()方法,把它作為引數傳入即可,

現在重新運行一下程式,并點擊Send notice按鈕,依舊會發出一條通知,然后下拉系統狀態欄,點擊一下該通知,就會看到NotificationActivity這個活動的界面了,如圖:

image

然而,點擊之后系統狀態上的通知圖示還沒有消失,如果我們沒有在代碼中對該通知進行取消,它就會一直顯示在系統的狀態欄上,

解決的方法有兩種,一種是在NotificationCompat.Builder中再連綴一個setAutoCancel()方法,一種是顯式地呼叫NotificationManager的cancel()方法將它取消,兩種方法我們都學習一下,第一種方法寫法如下:

Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        ...
                        .setAutoCancel(true)
                        .build();

可以看到,setAutoCancel()方法傳入true,就表示當點擊了這個通知的時候,通知會自動取消掉(親測有效),第二種方法寫法如下:

package com.zhouzhou.notificationtest;

import androidx.appcompat.app.AppCompatActivity;

import android.app.NotificationManager;
import android.os.Bundle;

public class NotificationActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.notification_layout);
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.cancel(1);
    }
}

親測有效!這個1是什么意思呢?還記得在創建通知的時候給每條通知指定的id嗎?當時我們給這條通知設定的id就是1,因此,如果你想取消哪條通知,在cancel()方法中傳入該通知的id就行了,

8.2.2 通知的進階技巧

上一小節中創建的通知屬于最基本的通知,實際上,NotificationCompat.Builder中提供了非常豐富的API來讓我們創建出更加多樣的通知效果,

從中選一些比較常用的API來進行學習,先來看看setSound()方法吧,它可以在通知發出的時候播放一段音頻,這樣就能夠更好地告知用戶有通知到來,setSound()方法接收一個Uri引數,所以在指定音頻檔案的時候還需要先獲取到音頻檔案對應的URI,比如說,每個手機的/system/media/audio/ringtones目錄下都有很多的音頻檔案,我們可以從中隨便選一個音頻檔案,那么在代碼中就可以這樣指定:

Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        ...
                        .setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg")))
                        .build();

除了允許播放音頻外,我們還可以在通知到來的時候讓手機進行振動,使用的是vibrate這個屬性,它是一個長整型的陣列,用于設定手機靜止和振動的時長,以毫秒為單位,下標為0的值表示手機靜止的時長,下標為1的值表示手機振動的時長,下標為2的值又表示手機靜止的時長,以此類推,所以,如果想要讓手機在通知到來的時候立刻振動1秒,然后靜止1秒,再振動1秒,代碼就可以寫成:

Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        ...
                        .setVibrate(new long[] {0,1000,1000,1000})
                        .build();

不過,想要控制手機振動還需要宣告權限,因此,我們還得編輯AndroidManifest.xml檔案,加入如下宣告:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.notificationtest"
    android:versionCode="1"
    android:versionName="1.0">
    <uses-permission android:name="android.permission.VIBRATE"/>
    ...
</manifest>

下面我們來看一下如何在通知到來時控制手機LED燈的顯示,

現在的手機基本上都會前置一個LED燈,當有未接電話或未讀短信,而此時手機又處于鎖屏狀態時,LED燈就會不停地閃爍,提醒用戶去查看,我們可以使用setLights()方法來實作這種效果,setLights()方法接收3個引數:

  • 第一個引數用于指定LED燈的顏色;
  • 第二個引數用于指定LED燈亮起的時長,以毫秒為單位;
  • 第三個引數用于指定LED燈暗去的時長,也是以毫秒為單位,

所以,當通知到來時,如果想要實作LED燈以綠色的燈光一閃一閃的效果,就可以寫成:

Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        ...
                        .setLights(Color.GREEN,1000,1000)
                        .build();

如果你不想進行那么多繁雜的設定,也可以直接使用通知的默認效果,它會根據當前手機的環境來決定播放什么鈴聲,以及如何振動,寫法如下:

Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        .setContentTitle("My notification")
                        .setContentText("Hello World!")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.drawable.small_icon)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.large_icon))
                        .setContentIntent(pendingIntent)
                        //.setAutoCancel(true)
                        //.setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg")))
                        //.setVibrate(new long[] {0,1000,1000,1000})
                        //.setLights(Color.GREEN,1000,1000)
                        .setDefaults(NotificationCompat.DEFAULT_ALL)
                        .build();

意,以上所涉及的這些進階技巧都要在手機上運行才能看得到效果,模擬器是無法表現出振動以及LED燈閃爍等功能的,

8.2.3 通知的高級功能

繼續觀察NotificationCompat.Builder這個類,你會發現里面還有很多API是我們沒有使用過的,下面我們就來學習一些更加強大的API的用法,從而構建出更加豐富的通知效果,

setStyle()方法,這個方法允許我們構建出富文本的通知內容,也就是說通知中不光可以有文字和圖示,還可以包含更多的東西,setStyle()方法接收一個NotificationCompat.Style引數,這個引數就是用來構建具體的富文本資訊的,如長文字、圖片等,

在開始使用setStyle()方法之前,我們先來做一個試驗吧,之前的通知內容都比較短,如果設定成很長的文字會是什么效果呢?比如這樣寫:

Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        .setContentTitle("My notification")
                        .setContentText("Learn how to build notifications,send and sync data, and use voice actions.Get the official Android IDE and developer tools to build apps for Android.")
                        ...
                        .build();

現在重新運行程式并觸發通知,效果如圖:

image

可以看到,通知內容是無法顯示完整的,多余的部分會用省略號來代替,其實這也很正常,因為通知的內容本來就應該言簡意賅,詳細內容放到點擊后打開的活動當中會更加合適,但是如果你真的非常需要在通知當中顯示一段長文字,Android也是支持的,通過setStyle()方法就可以做到,具體寫法如下:

Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        .setContentTitle("My notification")
                        .setStyle(new NotificationCompat.BigTextStyle().bigText("Learn how to build notifications,send and sync data, and use voice actions.Get the official Android IDE and developer tools to build apps for Android."))
                        ...
                        .build();

在setStyle()方法中創建了一個NotificationCompat.BigTextStyle物件,這個物件就是用于封裝長文字資訊的,我們呼叫它的bigText()方法并將文字內容傳入就可以了,再次重新運行程式并觸發通知:

image

除了顯示長文字之外,通知里還可以顯示一張大圖片,具體用法也是基本相似的,并測驗了用啥圖片合適,

下圖為,big_image=阿里巴巴力量圖:

image

下圖為,big_image2=正常的百度圖片截圖:

image

這樣我們就把setStyle()方法中的重要內容基本都掌握了,

setPriority()方法,它可以用于設定通知的重要程度,setPriority()方法接收一個整型引數用于設定這條通知的重要程度,一共有5個常量值可選:

  • PRIORITY_DEFAULT表示默認的重要程度,和不設定效果是一樣的;
  • PRIORITY_MIN表示最低的重要程度,系統可能只會在特定的場景才顯示這條通知,比如用戶下拉狀態欄的時候;
  • PRIORITY_LOW表示較低的重要程度,系統可能會將這類通知縮小,或改變其顯示的順序,將其排在更重要的通知之后;
  • PRIORITY_HIGH表示較高的重要程度,系統可能會將這類通知放大,或改變其顯示的順序,將其排在比較靠前的位置;
  • PRIORITY_MAX表示最高的重要程度,這類通知訊息必須要讓用戶立刻看到,甚至需要用戶做出回應操作,具體寫法如下:
Notification notification = new NotificationCompat.Builder(MainActivity.this,"default")
                        ...
                        .setPriority(NotificationCompat.PRIORITY_MAX)
                        .build();

這里我們將通知的重要程度設定成了最高,表示這是一條非常重要的通知,要求用戶必須立刻看到,現在重新運行一下程序,并點擊Send notice按鈕,效果如圖:

image

可以看到,這次的通知不是在系統狀態欄顯示一個小圖示了,而是彈出了一個橫幅,并附帶了通知的詳細內容,表示這是一條非常重要的通知,書中的展示效果圖片:

image

不管用戶現在是在玩游戲還是看電影,這條通知都會顯示在最上方,以此引起用戶的注意,當然,使用這類通知時一定要小心,確保你的通知內容的確是至關重要的,不然如果讓用戶產生反感的話,很可能會導致我們的應用程式被卸載,

8.3 呼叫攝像頭和相冊

平時在使用QQ或微信的時候經常要和別人分享圖片,這些圖片可以是用手機攝像頭拍的,也可以是從相冊中選取的,幾乎在每一個應用程式中都會有,那么本節我們就學習一下呼叫攝像頭和相冊方面的知識,

8.3.1 呼叫攝像頭拍照

現在很多的應用都會要求用戶上傳一張圖片來作為頭像,這時打開攝像頭拍張照是最簡單快捷的,下面就讓我們通過一個例子來學習一下,如何才能在應用程式里呼叫手機的攝像頭進行拍照,新建一個CameraAlbumTest專案,然后修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/take_photo"
        android:text="Take Photo"/>
    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/picture"
        android:layout_gravity="center_horizontal"/>
</LinearLayout>

可以看到,布局檔案中只有兩個控制元件,一個Button和一個ImageView,Button是用于打開攝像頭進行拍照的,而ImageView則是用于將拍到的圖片顯示出來,然后開始撰寫呼叫攝像頭的具體邏輯,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.cameraalbumtest;

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

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
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 {
    public static final int TAKE_PHOTO = 1;
    private ImageView picture;
    private Uri imageUri;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button takePhoto = (Button) findViewById(R.id.take_photo);
        takePhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //創建File物件,用于存盤拍照后的照片
                File outputImage = new File(getExternalCacheDir(),"output_image.jpg");
                try {
                    if (outputImage.exists()) {
                        outputImage.delete();
                    }
                    outputImage.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (Build.VERSION.SDK_INT >= 24) {
                    imageUri = FileProvider.getUriForFile(MainActivity.this,"" +
                            "com.zhouzhou.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);
            }
        });
    }

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

上述代碼稍微有點復雜,我們來仔細地分析一下,

在MainActivity中要做的第一件事自然是分別獲取到Button和ImageView的實體,并給Button注冊上點擊事件,然后在Button的點擊事件里開始處理呼叫攝像頭的邏輯,我們重點看一下這部分代碼,

首先這里創建了一個File物件,用于存放攝像頭拍下的圖片,這里我們把圖片命名為output_image.jpg,并將它存放在手機SD卡的應用關聯快取目錄下,

什么叫作應用關聯快取目錄呢?就是指SD卡中專門用于存放當前應用快取資料的位置,呼叫getExternalCacheDir()方法可以得到這個目錄,具體的路徑是/sdcard/Android/data//cache,

image

那么為什么要使用應用關聯快取目錄來存放圖片呢?因為從Android 6.0系統開始,讀寫SD卡被列為了危險權限,如果將圖片存放在SD卡的任何其他目錄,都要進行運行時權限處理才行,而使用應用關聯目錄則可以跳過這一步,

接著會進行一個判斷,如果運行設備的系統版本低于Android 7.0,就呼叫Uri的fromFile()方法將File物件轉換成Uri物件,這個Uri物件標識著output_image.jpg這張圖片的本地真實路徑,否則,就呼叫FileProvider的getUriForFile()方法將File物件轉換成一個封裝過的Uri物件,getUriForFile()方法接收3個引數:

  • 第一個引數要求傳入Context物件;

  • 第二個引數可以是任意唯一的字串,第三個引數則是我們剛剛創建的File物件,

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

接下來構建出了一個Intent物件,并將這個Intent的action指定為android.media. action.IMAGE_CAPTURE,再呼叫Intent的putExtra()方法指定圖片的輸出地址,這里填入剛剛得到的Uri物件,最后呼叫startActivityForResult()來啟動活動,

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

注意,剛才我們是使用startActivityForResult()來啟動活動的,因此拍完照后會有結果回傳到onActivityResult()方法中,如果發現拍照成功,就可以呼叫BitmapFactory的decodeStream()方法將output_image.jpg這張照片決議成Bitmap物件,然后把它設定到ImageView中顯示出來,不過現在還沒結束,剛才提到了內容提供器,那么我們自然要在AndroidManifest.xml中對內容提供器進行注冊了,如下所示:

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

    <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/Theme.CameraAlbumTest">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!--android:name="android.support.v4.content.FileProvider"爆紅
        解決辦法,需要將其換成androidx.core.content.FileProvider參考博客:https://blog.csdn.net/weixin_44047784/article/details/121609977-->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.zhouzhou.cameraalbumtest.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
    </application>
</manifest>

其中,android:name屬性的值是固定的,android:authorities屬性的值必須要和剛才FileProvider.getUriForFile()方法中的第二個引數一致,另外,這里還在<provider>標簽的內部使用<meta-data>來指定Uri的共享路徑,并參考了一個@xml/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這張圖片的路徑,

注意:上面的path屬性設定空值時,會有爆紅,但是不會影響運行結果,經測驗一樣OK,

image

不想爆紅,可以填斜杠(/)或著圖片的全路徑名稱,經過測驗了,都是OK的:

<?xml version="1.0" encoding="UTF-8" ?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 爆紅,不影響結果:<external-path name="my_images" path=""/> -->
    <!-- OK <external-path name="my_images" path="/"/> -->
    <!-- 圖片全路徑 -->
   <external-path name="my_images" path="/sdcard/Android/data/com.zhouzhou.cameraalbumtest/cache/output_image.jpg"/>
</paths>

另外,還有一點要注意,在Android 4.4系統之前,訪問SD卡的應用關聯目錄也是要宣告權限的,從4.4系統開始不再需要權限宣告,那么我們為了能夠兼容老版本系統的手機,還需要在AndroidManifest.xml中宣告一下訪問SD卡的權限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.cameraalbumtest">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    ...
    </application>
</manifest>

這樣代碼就都撰寫完了,現在將程式運行到手機上,然后點擊Take Photo按鈕就可以進行拍照了,如圖所示:

image

拍照完成后,點擊中間按鈕就會回到我們程式的界面,同時,拍攝的照片也會顯示出來了,如圖:

image

8.3.2 從相冊中選擇照片

直接從相冊里選取一張現有的照片會比打開相機拍一張照片更加常用,下面我們就來看一下,如何才能實作從相冊中選擇照片的功能,還是在CameraAlbumTest專案的基礎上進行修改,編輯activity_main.xml檔案,在布局中添加一個按鈕用于從相冊中選擇照片,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/take_photo"
        android:text="Take Photo"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/choose_from_album"
        android:text="Choose From Album"/>
    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/picture"
        android:layout_gravity="center_horizontal"/>
</LinearLayout>

然后修改MainActivity中的代碼,加入從相冊選擇照片的邏輯,代碼如下所示:

package com.zhouzhou.cameraalbumtest;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

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

public class MainActivity extends AppCompatActivity {
    public static final int TAKE_PHOTO = 1;
    public static final int CHOOSE_PHOTO = 2;
    private ImageView picture;
    private Uri imageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button takePhoto = (Button) findViewById(R.id.take_photo);
        Button chooseFromAlbum = (Button) findViewById(R.id.choose_from_album);
        picture = (ImageView) findViewById(R.id.picture);
        takePhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //創建File物件,用于存盤拍照后的照片
                File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
                try {
                    if (outputImage.exists()) {
                        outputImage.delete();
                    }
                    outputImage.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (Build.VERSION.SDK_INT < 24) {
                    imageUri = Uri.fromFile(outputImage);
                } else {
                    imageUri = FileProvider.getUriForFile(MainActivity.this, "com.zhouzhou.cameraalbumtest.fileprovider", outputImage);
                }
                //啟動相機程式
                Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                startActivityForResult(intent, TAKE_PHOTO);
            }
        });
        chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                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) {
        super.onRequestPermissionsResult(requestCode, permissions, 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;
            default:
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case TAKE_PHOTO:
                if (resultCode == RESULT_OK) {
                    try {
                        //將拍攝的照片顯示出來
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                        picture.setImageBitmap(bitmap);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case CHOOSE_PHOTO:
                if (resultCode == RESULT_OK) {
                    //判斷手機系統版本號
                    if (Build.VERSION.SDK_INT >= 19) {
                        //4.4及以上系統使用這個方法處理圖片
                        handleImageOnKitKat(data);
                    } else {
                        //4.4以下的系統使用這個方法處理圖片
                        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 contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public downloads"),Long.valueOf(docId));
                    imagePath = getImagePath(contentUri,null);
                } else if ("content".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);
    }

    @SuppressLint("Range")
    private String getImagePath(Uri uri, String selection) {
        String path = null;
        //通過Uri和Selection來獲取真是的路徑圖片
        Cursor cursor = getContentResolver().query(uri,null,selection,null,null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }
    private void displayImage(String imagePath) {
        if (imagePath != null) {
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
            picture.setImageBitmap(bitmap);
        } else {
            Toast.makeText(this,"failed to get image",Toast.LENGTH_SHORT).show();
        }
    }
}

可以看到,在Choose From Album按鈕的點擊事件里我們先是進行了一個運行時權限處理,動態申請WRITE_EXTERNAL_STORAGE這個危險權限,

為什么需要申請這個權限呢?因為相冊中的照片都是存盤在SD卡上的,要從SD卡中讀取照片就需要申請這個權限,WRITE_EXTERNAL_STORAGE表示同時授予程式對SD卡讀和寫的能力,當用戶授權了權限申請之后會呼叫openAlbum()方法,這里先是構建出了一個Intent物件,并將它的action指定為android.intent.action.GET_CONTENT,接著給這個Intent物件設定一些必要的引數,然后呼叫startActivityForResult()方法就可以打開相冊程式選擇照片了,

注意,在呼叫startActivityForResult()方法的時候,我們給第二個引數傳入的值變成了CHOOSE_PHOTO,這樣當從相冊選擇完圖片回到onActivityResult()方法時,就會進入CHOOSE_PHOTO的case來處理圖片,

接下來的邏輯就比較復雜了,首先為了兼容新老版本的手機,我們做了一個判斷,如果是4.4及以上系統的手機就呼叫handleImageOnKitKat()方法來處理圖片,否則就呼叫handleImageBeforeKitKat()方法來處理圖片,之所以要這樣做,是因為Android系統從4.4版本開始,選取相冊中的圖片不再回傳圖片真實的Uri了,而是一個封裝過的Uri,因此如果是4.4版本以上的手機就需要對這個Uri進行決議才行,

那么handleImageOnKitKat()方法中的邏輯就基本是如何決議這個封裝過的Uri了,這里有好幾種判斷情況,如果回傳的Uri是document型別的話,那就取出document id進行處理,如果不是的話,那就使用普通的方式處理,另外,如果Uri的authority是media格式的話,document id還需要再進行一次決議,要通過字串分割的方式取出后半部分才能得到真正的數字id,取出的id用于構建新的Uri和條件陳述句,然后把這些值作為引數傳入到getImagePath()方法當中,就可以獲取到圖片的真實路徑了,拿到圖片的路徑之后,再呼叫displayImage()方法將圖片顯示到界面上,

相比于handleImageOnKitKat()方法,handleImageBeforeKitKat()方法中的邏輯就要簡單得多了,因為它的Uri是沒有封裝過的,不需要任何決議,直接將Uri傳入到getImagePath()方法當中就能獲取到圖片的真實路徑了,最后同樣是呼叫displayImage()方法來讓圖片顯示到界面上,

現在將程式重新運行到手機上,然后點擊一下Choose From Album按鈕,首先會彈出權限申請框,如圖:

image

點擊允許之后就會打開手機相冊,如圖:

image

然后,隨意選擇一張照片,回到我們程式的界面,選中的照片應該就會顯示出來了,如圖:

image

不過,目前我們的實作還不算完美,因為某些照片即使經過裁剪后體積仍然很大,直接加載到記憶體中有可能會導致程式崩潰,更好的做法是根據專案的需求先對照片進行適當的壓縮,然后再加載到記憶體中,至于如何對照片進行壓縮,就要考驗你查閱資料的能力了,這里就不再展開進行講解了,

8.4 播放多媒體檔案

Android在播放音頻和視頻方面也是做了相當不錯的支持,它提供了一套較為完整的API,使得開發者可以很輕松地撰寫出一個簡易的音頻或視頻播放器,下面我們就來具體地學習一下,

8.4.1 播放音頻

在Android中播放音頻檔案一般都是使用MediaPlayer類來實作的,它對多種格式的音頻檔案提供了非常全面的控制方法,從而使得播放音樂的作業變得十分簡單,下表列出了MediaPlayer類中一些較為常用的控制方法,

image

單了解了上述方法后,我們再來梳理一下MediaPlayer的作業流程,首先需要創建出一個MediaPlayer物件,然后呼叫setDataSource()方法來設定音頻檔案的路徑,再呼叫prepare()方法使MediaPlayer進入到準備狀態,接下來呼叫start()方法就可以開始播放音頻,呼叫pause()方法就會暫停播放,呼叫reset()方法就會停止播放,

下面就讓我們通過一個具體的例子來學習一下吧,新建一個PlayAudioTest專案,然后修改activity_main.xml中的代碼,如下所示:

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

    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/play"
        android:text="Play"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/pause"
        android:text="Pause"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/stop"
        android:text="Stop"/>

</LinearLayout>

布局檔案中放置了3個按鈕,分別用于對音頻檔案進行播放、暫停和停止操作,然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.playaudiotest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.io.File;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private MediaPlayer mediaPlayer = 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(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this,new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },1);
        } else {
            initMediaPlayer();//初始化MediaPlayer
        }
    }
    private void initMediaPlayer() {
        try {
            //"Alien Invasion.mp3"是PC端下載好的音樂,然后通過360手機助手PC端放入到音樂——手機音樂,再傳入到手機
            File file = new File(Environment.getExternalStorageDirectory(),"Alien Invasion.mp3");
            mediaPlayer.setDataSource(file.getPath());//指定音頻檔案的路徑
            mediaPlayer.prepare();//讓MediaPlayer進入到準備狀態
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, 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:
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.play:
                if (! mediaPlayer.isPlaying()) {
                    mediaPlayer.start();//開始播放
                }
                break;
            case R.id.pause:
                if (mediaPlayer.isPlaying()) {
                    mediaPlayer.pause();//暫停播放
                } else {
                    mediaPlayer.start();//由暫停到播放
                }
                break;
            case R.id.stop:
                if (mediaPlayer.isPlaying()) {
                    mediaPlayer.reset();//停止播放
                    initMediaPlayer();
                }
                break;
            default:
                break;
        }
    }

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

可以看到,在類初始化的時候我們就先創建了一個MediaPlayer的實體,然后在onCreate()方法中進行了運行時權限處理,動態申請WRITE_EXTERNAL_STORAGE權限,這是由于待會我們會在SD卡中放置一個音頻檔案,程式為了播放這個音頻檔案必須擁有訪問SD卡的權限才行,下圖為在SD卡中放入一個音頻檔案:

image

注意,在onRequestPermissionsResult()方法中,如果用戶拒絕了權限申請,那么就呼叫finish()方法將程式直接關掉,因為如果沒有SD卡的訪問權限,我們這個程式將什么都干不了,

用戶同意授權之后就會呼叫initMediaPlayer()方法為MediaPlayer物件進行初始化操作,在initMediaPlayer()方法中,首先是通過創建一個File物件來指定音頻檔案的路徑,從這里可以看出,我們需要事先在SD卡的根目錄下放置一個名為Alien Invasion.mp3的音頻檔案,后面依次呼叫了setDataSource()方法和prepare()方法,為MediaPlayer做好了播放前的準備,

接下來,我們看一下各個按鈕的點擊事件中的代碼,

當點擊Play按鈕時會進行判斷,如果當前MediaPlayer沒有正在播放音頻,則呼叫start()方法開始播放,當點擊Pause按鈕時會判斷,如果當前MediaPlayer正在播放音頻,則呼叫pause()方法暫停播放,如果當前MediaPlayer沒有播放音頻,則呼叫start()方法開始播放,當點擊Stop按鈕時會判斷,如果當前MediaPlayer正在播放音頻,則呼叫reset()方法將MediaPlayer重置為剛剛創建的狀態,然后重新呼叫一遍initMediaPlayer()方法,

最后在onDestroy()方法中,我們還需要分別呼叫stop()方法和release()方法,將與MediaPlayer相關的資源釋放掉,另外,千萬不要忘記在AndroidManifest.xml檔案中宣告用到的權限,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.playaudiotest">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    ....
</manifest>

這樣一個簡易版的音樂播放器就完成了,現在將程式運行到手機上會先彈出權限申請框,如圖:

image

同意授權之后就可以開始播放音樂了,點擊一下Play按鈕,優美的音樂就會響起,然后點擊Pause按鈕,音樂就會停住,再次點擊Play按鈕(或者Pause按鈕),會接著暫停之前的位置繼續播放,這時如果點擊一下Stop按鈕,音樂也會停住,但是當再次點擊Play按鈕時,音樂就會從頭開始播放了,(已經全部測驗,完全OK!)

8.4.2 播放視頻

播放視頻檔案其實并不比播放音頻檔案復雜,主要是使用VideoView類來實作的,這個類將視頻的顯示和控制集于一身,使得我們僅僅借助它就可以完成一個簡易的視頻播放器,VideoView的用法和MediaPlayer也比較類似,主要有以下常用方法:

image

那么,我們還是通過一個實際的例子來學習一下吧,新建PlayVideoTest專案,然后修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_
        android:layout_height="wrap_content">
        <Button
            android:layout_
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:id="@+id/play"
            android:text="Play"/>
        <Button
            android:layout_
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:id="@+id/pause"
            android:text="Pause"/>
        <Button
            android:layout_
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:id="@+id/replay"
            android:text="Replay"/>
    </LinearLayout>
    <VideoView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/video_view"/>
</LinearLayout>

在這個布局檔案中,首先放置了3個按鈕,分別用于控制視頻的播放、暫停和重新播放,然后在按鈕下面又放置了一個VideoView,稍后的視頻就將在這里顯示,接下來修改MainActivity中的代碼,如下所示:

package com.zhouzhou.playvideotest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import android.widget.VideoView;

import java.io.File;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private VideoView videoView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        videoView = (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(),"何同學.mp4");
        videoView.setVideoPath(file.getPath());//指定視頻檔案的路徑
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, 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;
            default:
        }
    }

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

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (videoView != null) {
            videoView.suspend();
        }
    }
}

這部分代碼和前面播放音頻的代碼非常類似,

首先在onCreate()方法中同樣進行了一個運行時權限處理,因為視頻檔案將會放在SD卡上,當用戶同意授權了之后就會呼叫initVideoPath()方法來設定視頻檔案的路徑,這里我們需要事先在SD卡的根目錄下放置一個名為:何同學.mp4的視頻檔案,

下面看一下各個按鈕的點擊事件中的代碼,當點擊Play按鈕時會進行判斷,如果當前并沒有正在播放視頻,則呼叫start()方法開始播放,當點擊Pause按鈕時會判斷,如果當前視頻正在播放,則呼叫pause()方法暫停播放,當點擊Replay按鈕時會判斷,如果當前視頻正在播放,則呼叫resume()方法從頭播放視頻,最后在onDestroy()方法中,我們還需要呼叫一下suspend()方法,將VideoView所占用的資源釋放掉,

另外,仍然始終要記得在AndroidManifest.xml檔案中宣告用到的權限,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.playvideotest">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    ...
</manifest>

現在將程式運行到手機上,報錯:手機上跳出彈窗顯示”無法播放此視頻”

image

網上尋找解決辦法:

  1. 權限問題(uses-permission我設定了,所以不是這個問題)
  2. 視頻格式問題(說是要將視頻轉成.3gp格式,測驗不行)
  3. 視頻與Android不兼容(沒測)
  4. 手機上需要下載視頻播放器(該手機記憶體比較小,我確實洗掉了視頻播放器,但是,在最后測驗成功后,我洗掉了手機上的視頻播放器,該專案依然可以正常運行并播放視頻的,測驗完全OK,然而,此時再到相冊里面找到任何一個視頻,點擊播放都是無法播放的,可見有無視頻播放器對專案本身不影響,它影響手機能否播放相冊里面的視頻)
  5. 視頻不要下載網上的,說網上的視頻素材是做了保護措施的,且空間不要太大,最好自己錄制的(我用手機錄制一段,名叫“自錄視頻.mp4”的視頻,很短的一段視頻(3秒),測驗也不行)

.... 后來,我自己發現了問題所在:

1.我先查看了AS中的sdcard檔案夾,突然看到了自己上一個案例的音頻檔案:Alien Invasion.mp3,心里有底了,

既然都是SD卡中找檔案,為啥里面沒有我傳輸的視頻檔案,沒有檔案還顯示啥呢,

image

  1. 我使用了傳輸音頻一樣的方式,傳輸視頻檔案,如下圖:

image

  1. 看來這樣傳輸是不行的,接著我直接將網上下載的,比較大(12分鐘)的視頻檔案:“何同學.mp4”,粘貼到內置SD卡中:(注:等我測驗成功這個案例,查看手機,發現何同學.mp4這個視頻并不在用戶看到的手機相冊——相機視頻里面,而是在手機相冊——其他相冊——手機根目錄)

image

再看看AS中,有該視頻的:

image

現在將程式運行到手機上,會先彈出一個權限申請對話框(測驗OK),同意授權之后點擊一下Play按鈕,就可以看到視頻已經開始播放了,如圖:

image

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

為什么它的用法和MediaPlayer這么相似呢?其實VideoView只是幫我們做了一個很好的封裝而已,它的背后仍然是使用MediaPlayer來對視頻檔案進行控制的,

需要注意,VideoView并不是一個萬能的視頻播放工具類,它在視頻格式的支持以及播放效率方面都存在著較大的不足,所以,如果想要僅僅使用VideoView就撰寫出一個功能非常強大的視頻播放器是不太現實的,但是如果只是用于播放一些游戲的片頭影片,或者某個應用的視頻宣傳,使用VideoView還是綽綽有余的,

8.5 小結與點評

本章主要對Android系統中的各種多媒體技術進行了學習,其中包括通知的使用技巧、呼叫攝像頭拍照、從相冊中選取照片,以及播放音頻和視頻檔案,由于所涉及的多媒體技術在模擬器上很難看得到效果,因此本章中還特意講解了在Android手機上除錯程式的方法,

目前我們所學的所有東西都僅僅是在本地上進行的,而實際上幾乎市場上的每個應用都會涉及網路互動的部分,所以下一章中我們將會學習一下Android網路編程方面的內容,

個人學習筆記,針對本人在自學中遇到的問題,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472408.html

標籤:Android

上一篇:《第一行代碼:Android篇》學習筆記(七)

下一篇:《第一行代碼:Android篇》學習筆記(九)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more