จัดการโฟกัสเสียง

แอป Android ตั้งแต่ 2 แอปขึ้นไปสามารถเล่นเสียงไปยังสตรีมเอาต์พุตเดียวกันพร้อมกันได้ และระบบจะมิกซ์ทุกอย่างเข้าด้วยกัน แม้ว่าในทางเทคนิคแล้วจะน่าประทับใจ แต่ก็อาจสร้างความรำคาญใจให้แก่ผู้ใช้ได้มาก เพื่อหลีกเลี่ยงไม่ให้แอปเพลงทุกแอปเล่นพร้อมกัน Android จึงได้เปิดตัวแนวคิดเรื่องโฟกัสเสียง มีแอปเดียวเท่านั้นที่สามารถใช้โฟกัสเสียงได้ในแต่ละครั้ง

เมื่อแอปต้องการเอาต์พุตเสียง แอปควรขอโฟกัสเสียง เมื่อแท็บมีโฟกัส แท็บจะเล่นเสียงได้ อย่างไรก็ตาม หลังจากได้รับโฟกัสเสียงแล้ว คุณอาจไม่สามารถเก็บโฟกัสเสียงไว้ได้จนกว่าจะเล่นเสร็จ แอปอื่นสามารถขอโฟกัสได้ ซึ่งจะแทนที่การโฟกัสเสียงของคุณ หากเกิดกรณีดังกล่าว แอปควรหยุดเล่นชั่วคราวหรือลดระดับเสียงลงเพื่อให้ผู้ใช้ได้ยินแหล่งที่มาของเสียงใหม่ได้ง่ายขึ้น

ก่อน Android 12 (API ระดับ 31) ระบบจะไม่จัดการโฟกัสเสียง ดังนั้น แม้ว่าเราจะแนะนำให้นักพัฒนาแอปปฏิบัติตามหลักเกณฑ์การโฟกัสเสียง แต่หากแอปยังคงเล่นเสียงดังแม้ว่าจะสูญเสียการโฟกัสเสียงในอุปกรณ์ที่ใช้ Android 11 (ระดับ API 30) หรือต่ำกว่า ระบบก็ไม่สามารถป้องกันได้ อย่างไรก็ตาม ลักษณะการทำงานของแอปเช่นนี้ส่งผลให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ไม่ดี และมักทำให้ผู้ใช้ถอนการติดตั้งแอปที่ทำงานผิดปกติ

แอปเสียงที่ออกแบบมาอย่างดีควรจัดการโฟกัสเสียงตามหลักเกณฑ์ทั่วไปต่อไปนี้

  • เรียกใช้ requestAudioFocus() ทันทีก่อนเริ่มเล่นและยืนยันว่าการเรียกใช้แสดงผลเป็น AUDIOFOCUS_REQUEST_GRANTED เรียกใช้ requestAudioFocus() ใน Callback onPlay() ของเซสชันสื่อ

  • เมื่อแอปอื่นได้รับโฟกัสเสียง ให้หยุดหรือหยุดเล่นชั่วคราว หรือลดระดับเสียงลง

  • เมื่อการเล่นหยุดลง (เช่น เมื่อแอปไม่มีอะไรให้เล่นแล้ว) ให้ละทิ้งโฟกัสเสียง แอปของคุณไม่จำเป็นต้องละทิ้งโฟกัสเสียงหากผู้ใช้หยุดเล่นชั่วคราว แต่อาจกลับมาเล่นต่อในภายหลัง

  • ใช้ AudioAttributes เพื่ออธิบายประเภทเสียงที่แอปกำลังเล่น เช่น สำหรับแอปที่เล่นเสียงพูด ให้ระบุ CONTENT_TYPE_SPEECH

ระบบจะจัดการโฟกัสเสียงแตกต่างกันไปตามเวอร์ชันของ Android ที่ใช้งานอยู่ ดังนี้

Android 12 (ระดับ API 31) ขึ้นไป
ระบบจะจัดการโฟกัสเสียง ระบบจะบังคับให้การเล่นเสียงจากแอปค่อยๆ เบาลงเมื่อแอปอื่นขอโฟกัสเสียง นอกจากนี้ ระบบยังปิดเสียงการเล่นเมื่อมีสายเรียกเข้าด้วย
Android 8.0 (ระดับ API 26) ถึง Android 11 (ระดับ API 30)
ระบบไม่ได้จัดการโฟกัสเสียง แต่มีการเปลี่ยนแปลงบางอย่างที่เริ่มใช้ตั้งแต่ Android 8.0 (ระดับ API 26) เป็นต้นไป
Android 7.1 (ระดับ API 25) และต่ำกว่า
ระบบไม่ได้จัดการโฟกัสเสียง และแอปจะจัดการโฟกัสเสียงโดยใช้ requestAudioFocus() และ abandonAudioFocus()

โฟกัสเสียงใน Android 12 ขึ้นไป

แอปสื่อหรือแอปเกมที่ใช้โฟกัสเสียงไม่ควรเล่นเสียงหลังจากที่สูญเสียโฟกัส ใน Android 12 (ระดับ API 31) ขึ้นไป ระบบจะบังคับใช้ลักษณะการทำงานนี้ เมื่อแอปขอโฟกัสเสียงขณะที่แอปอื่นมีโฟกัสและกำลังเล่นอยู่ ระบบจะบังคับให้แอปที่เล่นอยู่ค่อยๆ เบาเสียงลง การเพิ่มการเฟดออกจะช่วยให้การเปลี่ยนจากแอปหนึ่งไปยังอีกแอปหนึ่งราบรื่นยิ่งขึ้น

ลักษณะการเฟดออกนี้จะเกิดขึ้นเมื่อเป็นไปตามเงื่อนไขต่อไปนี้

  1. แอปแรกที่กำลังเล่นอยู่มีคุณสมบัติตรงตามเกณฑ์ทั้งหมดต่อไปนี้

  2. แอปที่ 2 ขอโฟกัสเสียงด้วย AudioManager.AUDIOFOCUS_GAIN

เมื่อเป็นไปตามเงื่อนไขเหล่านี้ ระบบเสียงจะค่อยๆ เบาเสียงของแอปแรก เมื่อเสียงเบาลงจนสุดแล้ว ระบบจะแจ้งให้แอปแรกทราบว่าสูญเสียโฟกัส โปรแกรมเล่นของแอปจะยังคงปิดเสียงอยู่จนกว่าแอปจะขอโฟกัสเสียงอีกครั้ง

ลักษณะการทำงานของโฟกัสเสียงที่มีอยู่

นอกจากนี้ คุณควรทราบถึงกรณีอื่นๆ ต่อไปนี้ที่เกี่ยวข้องกับการเปลี่ยนโฟกัสเสียงด้วย

การลดเสียงอัตโนมัติ

การลดระดับเสียงอัตโนมัติ (การลดระดับเสียงของแอปหนึ่งชั่วคราวเพื่อให้ได้ยินเสียงของอีกแอปหนึ่งอย่างชัดเจน) เปิดตัวใน Android 8.0 (ระดับ API 26)

การให้ระบบใช้การลดระดับเสียงจะช่วยให้คุณไม่ต้องใช้การลดระดับเสียงในแอป

การลดระดับเสียงอัตโนมัติจะเกิดขึ้นเมื่อการแจ้งเตือนด้วยเสียงดึงดูดความสนใจจากแอปที่กำลังเล่นอยู่ด้วย การเริ่มเล่นการแจ้งเตือนจะซิงค์กับจุดสิ้นสุดของช่วงลดระดับเสียง

การลดระดับเสียงอัตโนมัติจะเกิดขึ้นเมื่อเป็นไปตามเงื่อนไขต่อไปนี้

  1. แอปแรกที่กำลังเล่นอยู่มีคุณสมบัติตรงตามเกณฑ์ทั้งหมดต่อไปนี้

  2. แอปที่ 2 ขอโฟกัสเสียงด้วย AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK

เมื่อเป็นไปตามเงื่อนไขเหล่านี้ ระบบเสียงจะลดระดับเสียงของเพลเยอร์ที่ทำงานอยู่ทั้งหมดของแอปแรกขณะที่แอปที่ 2 มีโฟกัส เมื่อแอปที่ 2 เลิกใช้โฟกัส ระบบจะเลิกหรี่เสียง แอปแรกจะไม่ได้รับการแจ้งเตือนเมื่อสูญเสียโฟกัส จึงไม่ต้องดำเนินการใดๆ

โปรดทราบว่าระบบจะไม่ลดระดับเสียงโดยอัตโนมัติเมื่อผู้ใช้กำลังฟังเนื้อหาที่เป็นคำพูด เนื่องจากผู้ใช้อาจพลาดเนื้อหาบางส่วนของรายการ เช่น การนำทางด้วยเสียงสำหรับเส้นทางการขับขี่จะไม่ถูกลดระดับเสียงลง

ปิดเสียงการเล่นเสียงปัจจุบันเมื่อมีสายเรียกเข้า

แอปบางแอปทำงานไม่ถูกต้องและยังคงเล่นเสียงต่อไปในระหว่างการโทร สถานการณ์นี้ทำให้ผู้ใช้ต้องค้นหาและปิดเสียงหรือออกจากแอปที่ทำให้เกิดปัญหาเพื่อฟังเสียงการโทร เพื่อป้องกันปัญหานี้ ระบบสามารถปิดเสียงจากแอปอื่นๆ ขณะมีสายเรียกเข้า ระบบจะเรียกใช้ฟีเจอร์นี้เมื่อมีสายเรียกเข้าและแอปเป็นไปตามเงื่อนไขต่อไปนี้

  • แอปมีแอตทริบิวต์การใช้งาน AudioAttributes.USAGE_MEDIA หรือ AudioAttributes.USAGE_GAME
  • แอปขอโฟกัสเสียงสำเร็จ (ได้รับโฟกัสใดๆ) และกำลังเล่นเสียงอยู่

หากแอปยังคงเล่นต่อระหว่างการโทร ระบบจะปิดเสียงการเล่นของแอปจนกว่าการโทรจะสิ้นสุดลง อย่างไรก็ตาม หากแอปเริ่มเล่นในระหว่างการโทร ระบบจะไม่ปิดเสียงเพลเยอร์นั้นเนื่องจากถือว่าผู้ใช้เริ่มเล่นโดยตั้งใจ

โฟกัสเสียงใน Android 8.0 ถึง Android 11

ตั้งแต่ Android 8.0 (ระดับ API 26) เป็นต้นไป เมื่อเรียกใช้ requestAudioFocus() คุณต้องระบุพารามิเตอร์ AudioFocusRequest AudioFocusRequest มีข้อมูลเกี่ยวกับบริบทเสียงและความสามารถของแอป ระบบจะใช้ข้อมูลนี้เพื่อจัดการการได้รับและการสูญเสียโฟกัสเสียงโดยอัตโนมัติ หากต้องการปล่อยโฟกัสเสียง ให้เรียกใช้เมธอด abandonAudioFocusRequest() ซึ่งจะใช้ AudioFocusRequest เป็นอาร์กิวเมนต์ด้วย ใช้อินสแตนซ์ AudioFocusRequest เดียวกันทั้งเมื่อคุณขอและละทิ้งโฟกัส

หากต้องการสร้าง AudioFocusRequest ให้ใช้ AudioFocusRequest.Builder เนื่องจากคำขอโฟกัสต้องระบุประเภทของคำขอเสมอ ประเภทจึงรวมอยู่ในคอนสตรัคเตอร์สำหรับเครื่องมือสร้าง ใช้วิธีการของ Builder เพื่อตั้งค่าช่องอื่นๆ ของคำขอ

ต้องกรอกข้อมูลในช่อง FocusGain ส่วนช่องอื่นๆ ทั้งหมดจะกรอกหรือไม่ก็ได้

วิธีการหมายเหตุ
setFocusGain() ต้องระบุข้อมูลในช่องนี้ในทุกคำขอ โดยจะใช้ค่าเดียวกับ durationHint ที่ใช้ในการเรียก requestAudioFocus() ก่อน Android 8.0 ซึ่งได้แก่ AUDIOFOCUS_GAIN, AUDIOFOCUS_GAIN_TRANSIENT, AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK หรือ AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
setAudioAttributes() AudioAttributes อธิบายกรณีการใช้งานสำหรับแอปของคุณ ระบบจะดูแอตทริบิวต์เหล่านี้เมื่อแอปได้รับและสูญเสียโฟกัสเสียง แอตทริบิวต์จะแทนที่แนวคิดของประเภทสตรีม ใน Android 8.0 (ระดับ API 26) ขึ้นไป ประเภทสตรีมสำหรับการดำเนินการอื่นๆ นอกเหนือจากการควบคุมระดับเสียงจะเลิกใช้งานแล้ว ใช้แอตทริบิวต์เดียวกันในคำขอโฟกัสที่คุณใช้ในโปรแกรมเล่นเสียง (ดังที่แสดงในตัวอย่างต่อจากตารางนี้)

ใช้ AudioAttributes.Builder เพื่อระบุแอตทริบิวต์ก่อน จากนั้นใช้วิธีนี้เพื่อกำหนดแอตทริบิวต์ให้กับคำขอ

หากไม่ได้ระบุไว้ AudioAttributes จะใช้ AudioAttributes.USAGE_MEDIA เป็นค่าเริ่มต้น

setWillPauseWhenDucked() เมื่อแอปอื่นขอโฟกัสด้วย AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK แอปที่มีโฟกัสอยู่มักจะไม่ได้รับ Callback onAudioFocusChange() เนื่องจากระบบสามารถลดระดับเสียงได้เอง เมื่อต้องการหยุดเล่นชั่วคราวแทนที่จะลดระดับเสียง ให้เรียกใช้ setWillPauseWhenDucked(true) และสร้างและตั้งค่า OnAudioFocusChangeListener ตามที่อธิบายไว้ในการลดระดับเสียงอัตโนมัติ
setAcceptsDelayedFocusGain() คำขอโฟกัสเสียงอาจไม่สำเร็จเมื่อโฟกัสถูกล็อกโดยแอปอื่น เมธอดนี้จะเปิดใช้การรับโฟกัสที่ล่าช้า ซึ่งก็คือความสามารถ ในการรับโฟกัสแบบอะซิงโครนัสเมื่อพร้อมใช้งาน

โปรดทราบว่าการได้รับโฟกัสที่ล่าช้าจะทำงานก็ต่อเมื่อคุณระบุ AudioManager.OnAudioFocusChangeListener ในคำขอเสียงด้วย เนื่องจากแอปของคุณต้องรับ Callback เพื่อให้ทราบว่าได้รับโฟกัสแล้ว

setOnAudioFocusChangeListener() คุณต้องระบุ OnAudioFocusChangeListener ก็ต่อเมื่อระบุ willPauseWhenDucked(true) หรือ setAcceptsDelayedFocusGain(true) ในคำขอด้วย

การตั้งค่า Listener มี 2 วิธี ได้แก่ วิธีที่มีและวิธีที่ไม่มีอาร์กิวเมนต์ของแฮนเดิล ตัวแฮนเดิลคือชุดข้อความที่ Listener ทำงานอยู่ หากคุณไม่ได้ระบุแฮนเดิล ระบบจะใช้แฮนเดิลที่เชื่อมโยงกับ Looper หลัก

ตัวอย่างต่อไปนี้แสดงวิธีใช้ AudioFocusRequest.Builder เพื่อสร้าง AudioFocusRequest รวมถึงขอและละทิ้งโฟกัสเสียง

Kotlin

// initializing variables for audio focus and playback management
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
    setAudioAttributes(AudioAttributes.Builder().run {
        setUsage(AudioAttributes.USAGE_GAME)
        setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        build()
    })
    setAcceptsDelayedFocusGain(true)
    setOnAudioFocusChangeListener(afChangeListener, handler)
    build()
}
val focusLock = Any()

var playbackDelayed = false
var playbackNowAuthorized = false

// requesting audio focus and processing the response
val res = audioManager.requestAudioFocus(focusRequest)
synchronized(focusLock) {
    playbackNowAuthorized = when (res) {
        AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
        AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
            playbackNow()
            true
        }
        AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
            playbackDelayed = true
            false
        }
        else -> false
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
override fun onAudioFocusChange(focusChange: Int) {
    when (focusChange) {
        AudioManager.AUDIOFOCUS_GAIN ->
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false
                    resumeOnFocusGain = false
                }
                playbackNow()
            }
        AudioManager.AUDIOFOCUS_LOSS -> {
            synchronized(focusLock) {
                resumeOnFocusGain = false
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying()
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // ... pausing or ducking depends on your app
        }
    }
}

Java

// initializing variables for audio focus and playback management
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_GAME)
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(playbackAttributes)
        .setAcceptsDelayedFocusGain(true)
        .setOnAudioFocusChangeListener(afChangeListener, handler)
        .build();
final Object focusLock = new Object();

boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;

// requesting audio focus and processing the response
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock) {
    if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
        playbackNowAuthorized = false;
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        playbackNowAuthorized = true;
        playbackNow();
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
        playbackDelayed = true;
        playbackNowAuthorized = false;
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
@Override
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false;
                    resumeOnFocusGain = false;
                }
                playbackNow();
            }
            break;
        case AudioManager.AUDIOFOCUS_LOSS:
            synchronized(focusLock) {
                resumeOnFocusGain = false;
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            synchronized(focusLock) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying();
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // ... pausing or ducking depends on your app
            break;
        }
    }
}

การลดเสียงอัตโนมัติ

ใน Android 8.0 (ระดับ API 26) เมื่อแอปอื่นขอโฟกัสด้วย AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK ระบบจะลดและคืนค่าระดับเสียงได้โดยไม่ต้องเรียกใช้ Callback onAudioFocusChange() ของแอป

แม้ว่าการลดระดับเสียงอัตโนมัติจะเป็นลักษณะการทำงานที่ยอมรับได้สำหรับแอปเล่นเพลงและวิดีโอ แต่ก็ไม่มีประโยชน์เมื่อเล่นเนื้อหาที่พูด เช่น ในแอปหนังสือเสียง ในกรณีนี้ แอปควรหยุดชั่วคราวแทน

หากต้องการให้แอปหยุดชั่วคราวเมื่อระบบขอให้ลดระดับเสียงแทนที่จะลดระดับเสียง ให้สร้าง OnAudioFocusChangeListener ด้วยเมธอด Callback onAudioFocusChange() ที่ใช้ลักษณะการทำงานหยุดชั่วคราว/เล่นต่อที่ต้องการ เรียกใช้ setOnAudioFocusChangeListener() เพื่อลงทะเบียน Listener และเรียกใช้ setWillPauseWhenDucked(true) เพื่อบอกให้ระบบใช้ Callback แทนการลดเสียงอัตโนมัติ

การรับโฟกัสล่าช้า

บางครั้งระบบไม่สามารถให้สิทธิ์คำขอโฟกัสเสียงได้เนื่องจากโฟกัสถูก "ล็อก" โดยแอปอื่น เช่น ระหว่างการโทร ในกรณีนี้ requestAudioFocus() จะแสดงผลเป็น AUDIOFOCUS_REQUEST_FAILED เมื่อเกิดกรณีนี้ขึ้น แอปของคุณไม่ควรเล่นเสียงต่อเนื่องจากไม่ได้รับโฟกัส

เมธอด setAcceptsDelayedFocusGain(true) ที่ช่วยให้แอปจัดการคำขอโฟกัสแบบอะซิงโครนัสได้ เมื่อตั้งค่าแฟล็กนี้ คำขอที่ส่งเมื่อโฟกัสถูกล็อกจะแสดงผลเป็น AUDIOFOCUS_REQUEST_DELAYED เมื่อไม่มีเงื่อนไขที่ล็อกโฟกัสเสียงอีกต่อไป เช่น เมื่อการโทรสิ้นสุดลง ระบบจะให้สิทธิ์คำขอโฟกัสที่รอดำเนินการและเรียกใช้ onAudioFocusChange() เพื่อแจ้งเตือนแอปของคุณ

หากต้องการจัดการการได้รับโฟกัสที่ล่าช้า คุณต้องสร้าง OnAudioFocusChangeListener ที่มีเมธอด Callback onAudioFocusChange() ซึ่งใช้ลักษณะการทำงานที่ต้องการ และลงทะเบียน Listener โดยเรียกใช้ setOnAudioFocusChangeListener()

โฟกัสเสียงใน Android 7.1 และต่ำกว่า

เมื่อเรียกใช้ requestAudioFocus() คุณต้องระบุคำแนะนำระยะเวลา ซึ่งอาจได้รับการยอมรับจากแอปอื่นที่กำลังโฟกัสและเล่นอยู่

  • ขอโฟกัสเสียงแบบถาวร (AUDIOFOCUS_GAIN) เมื่อคุณวางแผนที่จะเล่นเสียงในอนาคตอันใกล้ (เช่น เมื่อเล่นเพลง) และคาดหวังให้ผู้ถือโฟกัสเสียงก่อนหน้าหยุดเล่น
  • ขอโฟกัสชั่วคราว (AUDIOFOCUS_GAIN_TRANSIENT) เมื่อคุณคาดว่าจะเล่นเสียงเป็นระยะเวลาสั้นๆ และคาดหวังให้ผู้ถือครองก่อนหน้าหยุดเล่นชั่วคราว
  • ขอโฟกัสชั่วคราวด้วยการลดระดับเสียง (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) เพื่อระบุว่าคุณคาดว่าจะเล่นเสียงเป็นเวลาสั้นๆ เท่านั้น และเจ้าของโฟกัสก่อนหน้าจะเล่นต่อได้หาก "ลดระดับเสียง" (ลดระดับเสียง) เอาต์พุตเสียง เอาต์พุตเสียงทั้ง 2 รายการจะผสมกันในสตรีมเสียง การลดระดับเสียงเหมาะอย่างยิ่งสำหรับแอปที่ใช้สตรีมเสียงเป็นระยะๆ เช่น เส้นทางการขับขี่ที่ได้ยิน

เมธอด requestAudioFocus() ยังต้องใช้ AudioManager.OnAudioFocusChangeListener ด้วย ควรสร้าง Listener นี้ในกิจกรรมหรือบริการเดียวกันกับที่เป็นเจ้าของเซสชันสื่อ โดยจะใช้ Callback onAudioFocusChange() ที่แอปของคุณได้รับเมื่อแอปอื่นๆ ได้รับหรือละทิ้งโฟกัสเสียง

ข้อมูลโค้ดต่อไปนี้จะขอโฟกัสเสียงถาวรในสตรีม STREAM_MUSIC และลงทะเบียน OnAudioFocusChangeListener เพื่อจัดการ การเปลี่ยนแปลงที่ตามมาในโฟกัสเสียง (เราจะพูดถึง Listener การเปลี่ยนแปลงในหัวข้อการตอบสนองต่อการเปลี่ยนแปลงโฟกัสเสียง)

Kotlin

audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener

...
// Request audio focus for playback
val result: Int = audioManager.requestAudioFocus(
        afChangeListener,
        // Use the music stream.
        AudioManager.STREAM_MUSIC,
        // Request permanent focus.
        AudioManager.AUDIOFOCUS_GAIN
)

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

Java

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;

...
// Request audio focus for playback
int result = audioManager.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
}

เมื่อเล่นเสร็จแล้ว ให้เรียกใช้ abandonAudioFocus()

Kotlin

audioManager.abandonAudioFocus(afChangeListener)

Java

// Abandon audio focus when playback complete
audioManager.abandonAudioFocus(afChangeListener);

ซึ่งจะแจ้งให้ระบบทราบว่าคุณไม่ต้องการโฟกัสอีกต่อไป และยกเลิกการลงทะเบียน OnAudioFocusChangeListener ที่เกี่ยวข้อง หากคุณขอโฟกัสชั่วคราว ระบบจะแจ้งแอปที่หยุดชั่วคราวหรือลดระดับเสียงลงว่าสามารถเล่นต่อหรือคืนค่าระดับเสียงได้

การตอบสนองต่อการเปลี่ยนแปลงโฟกัสเสียง

เมื่อแอปได้รับโฟกัสเสียง แอปต้องสามารถปล่อยโฟกัสเสียงได้เมื่อแอปอื่นขอโฟกัสเสียงสำหรับตัวเอง เมื่อเกิดกรณีนี้ขึ้น แอปของคุณจะได้รับการเรียกใช้เมธอด onAudioFocusChange() ใน AudioFocusChangeListener ที่คุณระบุไว้เมื่อแอปเรียกใช้ requestAudioFocus()

พารามิเตอร์ focusChange ที่ส่งไปยัง onAudioFocusChange() จะระบุประเภทการเปลี่ยนแปลงที่เกิดขึ้น ซึ่งจะสอดคล้องกับคำแนะนำระยะเวลาที่ใช้โดยแอปที่กำลังรับโฟกัส แอปของคุณควรตอบสนองอย่างเหมาะสม

สูญเสียโฟกัสชั่วคราว
หากการเปลี่ยนแปลงโฟกัสเป็นแบบชั่วคราว (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK หรือ AUDIOFOCUS_LOSS_TRANSIENT) แอปควรลดระดับเสียง (หากคุณไม่ได้ใช้การลดระดับเสียงอัตโนมัติ) หรือหยุดเล่นชั่วคราว แต่ยังคงรักษาสถานะเดิมไว้

ในระหว่างที่สูญเสียโฟกัสเสียงชั่วคราว คุณควรตรวจสอบการเปลี่ยนแปลงในโฟกัสเสียงต่อไป และเตรียมพร้อมที่จะกลับมาเล่นตามปกติเมื่อได้รับโฟกัสกลับมา เมื่อแอปที่บล็อกละทิ้งโฟกัส คุณจะได้รับ Callback (AUDIOFOCUS_GAIN) ซึ่งคุณสามารถคืนค่าระดับเสียงเป็นระดับปกติหรือเริ่มเล่นอีกครั้งได้

สูญเสียโฟกัสอย่างถาวร
หากการสูญเสียโฟกัสเสียงเป็นแบบถาวร (AUDIOFOCUS_LOSS) แสดงว่าแอปอื่นกำลังเล่นเสียงอยู่ แอปของคุณควรหยุดเล่นชั่วคราวทันที เนื่องจากจะไม่ได้รับ Callback AUDIOFOCUS_GAIN หากต้องการเริ่มเล่นอีกครั้ง ผู้ใช้ต้องดำเนินการอย่างชัดเจน เช่น กดตัวควบคุมการเล่นใน UI ของการแจ้งเตือนหรือแอป

ข้อมูลโค้ดต่อไปนี้แสดงวิธีใช้ OnAudioFocusChangeListener และ Callback onAudioFocusChange() โปรดสังเกตการใช้ Handler เพื่อหน่วงเวลา Callback ของการหยุดเมื่อสูญเสียโฟกัสเสียงอย่างถาวร

Kotlin

private val handler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
    when (focusChange) {
        AudioManager.AUDIOFOCUS_LOSS -> {
            // Permanent loss of audio focus
            // Pause playback immediately
            mediaController.transportControls.pause()
            // Wait 30 seconds before stopping playback
            handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            // Pause playback
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // Lower the volume, keep playing
        }
        AudioManager.AUDIOFOCUS_GAIN -> {
            // Your app has been granted audio focus again
            // Raise volume to normal, restart playback if necessary
        }
    }
}

Java

private Handler handler = new Handler();
AudioManager.OnAudioFocusChangeListener afChangeListener =
  new AudioManager.OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
      if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
        // Permanent loss of audio focus
        // Pause playback immediately
        mediaController.getTransportControls().pause();
        // Wait 30 seconds before stopping playback
        handler.postDelayed(delayedStopRunnable,
          TimeUnit.SECONDS.toMillis(30));
      }
      else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
        // Pause playback
      } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
        // Lower the volume, keep playing
      } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
        // Your app has been granted audio focus again
        // Raise volume to normal, restart playback if necessary
      }
    }
  };

เครื่องจัดการใช้ Runnable ที่มีลักษณะดังนี้

Kotlin

private var delayedStopRunnable = Runnable {
    mediaController.transportControls.stop()
}

Java

private Runnable delayedStopRunnable = new Runnable() {
    @Override
    public void run() {
        getMediaController().getTransportControls().stop();
    }
};

หากต้องการตรวจสอบว่าการหยุดที่ล่าช้าจะไม่เริ่มทำงานหากผู้ใช้เริ่มเล่นอีกครั้ง ให้เรียกใช้ mHandler.removeCallbacks(mDelayedStopRunnable) เพื่อตอบสนองต่อการเปลี่ยนแปลงสถานะ เช่น เรียกใช้ removeCallbacks() ใน onPlay(), onSkipToNext() ฯลฯ ของ Callback คุณควรเรียกใช้เมธอดนี้ใน Callback onDestroy() ของบริการด้วยเมื่อล้างทรัพยากรที่บริการใช้