首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Android 自定義視頻播放控件:仿微信 SurfaceView + MediaPlayer 實作

     編輯:Java 相關知識

專案簡介

Android 原生 VideoView 功能有限,難以實現微信般的沉浸式播放體驗(如全屏手勢控制、邊緣縮放、彈幕覆蓋等)。本文實作一個繼承 SurfaceView 的自定義播放控件 MoviePlayerView,內部封裝 MediaPlayer,提供乾淨的播放/釋放/完成回調 API,適合作為影音類 App 的基礎播放組件。

核心類別:MoviePlayerView

package com.example.player;

import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MoviePlayerView extends SurfaceView 
        implements SurfaceHolder.Callback, MediaPlayer.OnCompletionListener {

    private SurfaceHolder holder;
    private MediaPlayer mediaPlayer;
    private OnPlayCompletionListener completionListener;
    private String pendingPath; // 等待 Surface 建立後播放的路徑

    public MoviePlayerView(Context context) { this(context, null); }
    public MoviePlayerView(Context context, AttributeSet attrs) { this(context, attrs, 0); }
    public MoviePlayerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        holder = getHolder();
        holder.addCallback(this);
        // 必須設定,否則部分舊機型無法顯示
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    /** 公開播放介面 */
    public void play(String filePath, OnPlayCompletionListener listener) {
        this.completionListener = listener;
        if (holder.getSurface().isValid()) {
            startPlayback(filePath);
        } else {
            pendingPath = filePath; // Surface 尚未就緒,稍後在 surfaceCreated 執行
        }
    }

    private void startPlayback(String filePath) {
        releasePlayer(); // 確保乾淨狀態
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mediaPlayer.setDisplay(holder); // 關鍵:綁定 Surface
        mediaPlayer.setOnCompletionListener(this);

        try {
            mediaPlayer.setDataSource(filePath);
            mediaPlayer.prepareAsync(); // 非同步準備,避免阻塞 UI 執行緒
            mediaPlayer.setOnPreparedListener(mp -> mp.start());
        } catch (IOException e) {
            e.printStackTrace();
            if (completionListener != null) completionListener.onPlayCompletion(); // 視為播放結束
        }
    }

    /** SurfaceHolder.Callback */
    @Override public void surfaceCreated(SurfaceHolder holder) {
        this.holder = holder;
        if (pendingPath != null) {
            startPlayback(pendingPath);
            pendingPath = null;
        }
    }
    @Override public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        this.holder = holder; // 尺寸變更更新 holder
    }
    @Override public void surfaceDestroyed(SurfaceHolder holder) {
        this.holder = null;
        releasePlayer(); // Surface 銷毀時釋放播放器
    }

    /** MediaPlayer.OnCompletionListener */
    @Override public void onCompletion(MediaPlayer mp) {
        releasePlayer();
        if (completionListener != null) completionListener.onPlayCompletion();
    }

    /** 釋放資源 */
    public void release() { releasePlayer(); }

    private void releasePlayer() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }

    /** 播放完成回調介面 */
    public interface OnPlayCompletionListener {
        void onPlayCompletion();
    }
}

關鍵設計決策解析

決策點選擇理由
繼承 SurfaceView 而非 TextureViewSurfaceView硬體覆蓋層,效能更低延遲、功耗更低;TextureView 需 GPU 合成,適合需動畫變換的場景
prepareAsync() 非同步準備是避免主執行緒卡頓(ANR),大檔案/網路流準備可能耗時秒級
setOnPreparedListener 啟動播放是確保 MediaPlayer 進入 Prepared 狀態後再 start,否則會拋 IllegalStateException
surfaceDestroyed 釋放 MediaPlayer是Surface 銷毀意味著顯示面消失,必須釋放解碼器與硬體資源,否則洩漏
回調介面 OnPlayCompletionListener自定義解耦播放器與業務邏輯(如自動播放下一條、顯示推薦頁)
Pending Path 機制是處理「先呼叫 play(),Surface 稍後才建立」的競態條件

XML 佈局使用範例

<com.example.player.MoviePlayerView
    android:id="@+id/moviePlayer"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_gravity="center" />

Activity 整合範例

public class PlayerActivity extends AppCompatActivity {
    private MoviePlayerView playerView;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);
        playerView = findViewById(R.id.moviePlayer);

        String videoUrl = "https://example.com/video.mp4"; // 或本地路徑 /sdcard/xxx.mp4
        playerView.play(videoUrl, () -> runOnUiThread(() -> {
            Toast.makeText(this, "播放完成", Toast.LENGTH_SHORT).show();
            // 此處可實作:播放下一條、顯示互動卡片等
        }));
    }

    @Override protected void onDestroy() {
        super.onDestroy();
        playerView.release(); // 必須釋放,否則記憶體洩漏
    }
}

進階擴充方向

  • 播放控制器 UI:疊加 FrameLayout 包含播放/暫停、進度條、全螢幕按鈕,監聽 MediaPlayer.OnBufferingUpdateListener、OnSeekCompleteListener。
  • 手勢控制:覆寫 onTouchEvent,左右滑動求進/退,上下滑動音量/亮度(參考 ijkplayer/ExoPlayer 控制器邏輯)。
  • 彈幕/字幕支援:在 dispatchDraw 或獨立 TextureView 覆蓋層繪製文字軌跡。
  • 網路流適配:整合 ExoPlayer 或 ijkplayer 替換 MediaPlayer,支援 DASH/HLS/平滑串流、快取策略。
  • 畫中畫:Android 8.0+ enterPictureInPictureMode(),配合 MediaSessionCompat 處理媒體按鍵。

常見坑與解決

現象原因解決
黑屏無畫面、聲音正常Surface 未正確綁定、或 PixelFormat 不支援確認 holder.setType(PUSH_BUFFERS)、Activity 設定 getWindow().setFormat(PixelFormat.TRANSLUCENT)
旋轉螢幕後播放異常Activity 重建導致 Surface 變更在 surfaceChanged 更新 holder,或鎖定螢幕方向 android:screenOrientation="landscape"
釋放後再次播放崩潰MediaPlayer 狀態機違規嚴格遵循 reset() → setDataSource → prepare → start,或直接 new 新實例
記憶體洩漏 (LeakCanary 偵測)未在 onDestroy/release() 呼叫 mediaPlayer.release()生命週期綁定:Activity/Fragment onDestroy 統一釋放

結語

MoviePlayerView 以不足 100 行核心程式碼,封裝了 SurfaceView + MediaPlayer 的標準播放流程與生命週期管理,提供乾淨的介面供上層業務擴充。理解其狀態機與 Surface 互動細節,是進階客製化播放器(如接入 ExoPlayer、實現微信級互動體驗)的必經基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved