[Android基礎] VideoView

我啥時候說啦jj發表於2017-12-27

專案需要做一個簡單的播放視訊功能demo,後期會換成公司自己的元件,所以就沒考慮使用第三方庫了,直接上系統的VideoView,在這裡記錄下操作; 順便吐槽下:一直都聽說簡書編輯器好用,第一次使用,有點失望,markdown跟效果分欄竟然不能同步滾動,也不支援[TOC],沒有個目錄實在很不習慣,表情圖示也不能插入,程式碼區塊加空行經常都識別不了啊,就不能讓我簡單滴從筆記中直接貼上md文字嗎... ==!

Demo專案下載 自己封裝了一個播放器

VideoView

資源

  1. Android三種播放視訊的方式
  2. Android播放器框架分析之AwesomePlayer
  3. 音訊與視訊播放 講的player類,比較全
  4. Android視訊播放器實現小視窗和全屏狀態切換

視訊播放原理: 系統會首先確定視訊的格式,然後得到視訊的編碼..然後對編碼進行解碼,得到一幀一幀的影象,最後在畫布上進行迅速更新,顯然需要在獨立的執行緒中完成,這時就需要使用surfaceView了

android 支援的編碼格式

android_video_support_format.png

基本使用

VideoView mVv = (VideoView) findViewById(R.id.vv);
//新增播放控制條,還是自定義好點
mVv.setMediaController(new MediaController(this));

//設定視訊源播放res/raw中的檔案,檔名小寫字母,格式: 3gp,mp4等,flv的不一定支援;
Uri rawUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.shuai_dan_ge);
mVv.setVideoURI(rawUri);

// 播放線上視訊
mVideoUri = Uri.parse("http://****/abc.mp4");
mVv.setVideoPath(mVideoUri.toString());

mVv.start();
mVv.requestFocus();

/*
其他方法:
mVv.pause();
mVv.stop();
mVv.resume();
mVv.setOnPreparedListener(this);
mVv.setOnErrorListener(this);
mVv.setOnCompletionListener(this);**

Error資訊處理 :
經常會碰到視訊編碼格式不支援的情況,這裡還是處理一下,若不想彈出提示框就返回true;
http://developer.android.com/intl/zh-cn/reference/android/media/MediaPlayer.OnErrorListener.html

@Override 
public boolean onError(MediaPlayer mp, int what, int extra) { 
   if(what==MediaPlayer.MEDIA_ERROR_SERVER_DIED){ 
        Log.v(TAG,"Media Error,Server Died"+extra); 
   }else if(what==MediaPlayer.MEDIA_ERROR_UNKNOWN){ 
        Log.v(TAG,"Media Error,Error Unknown "+extra); 
   } 
return true; 
} 
*/
複製程式碼

錯誤資訊

QCMediaPlayer.java

//常見錯誤: "無法播放此視訊" -我測試的是:紅米1s電信版4.4.4無法播放,但在三星s6(5.1.1)上就可以播放
//播放源:http://27.152.191.198/c12.e.99.com/b/p/67/c4ff9f6535ac41a598bb05bf5b05b185/c4ff9f6535ac41a598bb05bf5b05b185.v.854.480.f4v
MediaPlayer-JNI: QCMediaPlayer mediaplayer NOT present
MediaPlayer: Unable to create media player
MediaPlayer: Couldn't open file on client side, trying server side
MediaPlayer: error (1, -2147483648)
MediaPlayer: Error (1,-2147483648)
複製程式碼

有人說 用下面的方式可以處理該異常,但我是使用系統封裝好的控制元件,這個操作不到吧? 先記錄下:

MediaPlayer player = MediaPlayer.create(this, Uri.parse(sound_file_path));
MediaPlayer player = MediaPlayer.create(this, soundRedId, loop);
複製程式碼

全屏播放 - 橫豎屏切換

  • androidmanifest.xml 中依然還是定義豎屏,並定義一個切換橫縱屏按鈕 btnChange :
<activity
    android:name="lynxz.org.video.VideoActivity"
    android:configChanges="keyboard|orientation|screenSize"
    android:screenOrientation="portrait"
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
複製程式碼
  • 佈局:需要在 VidioView 外層套一個容器,比如:
    <RelativeLayout
        android:id="@+id/rl_vv"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@android:color/black"
        android:minHeight="200dp"
        android:visibility="visible">

        <VideoView
            android:id="@+id/vv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"/>
    </RelativeLayout>
複製程式碼

這麼做是為了在切換螢幕方向的時候對 rl_vv 進行拉伸,而內部的 VideoView 會依據視訊尺寸重新計算寬高,我們看看其 onMeasure() 原始碼就明瞭了,但若是直接具體指定了view的寬高,則視訊會被拉伸:

//VideoView.java 
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
    int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
    ......
    setMeasuredDimension(width, height);
}
複製程式碼
  • 按鈕監聽,手動切換
btnSwitch.setOnClickListener(View -> {
    if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    } else {
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    }
});
複製程式碼

設定VideoView佈局尺寸

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    if (mVv == null) {
        return;
    }
    if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){//橫屏
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().getDecorView().invalidate();
        float height = DensityUtil.getWidthInPx(this);
        float width = DensityUtil.getHeightInPx(this);
        mRlVv.getLayoutParams().height = (int) width;
        mRlVv.getLayoutParams().width = (int) height;
    } else {
        final WindowManager.LayoutParams attrs = getWindow().getAttributes();
        attrs.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().setAttributes(attrs);
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        float width = DensityUtil.getWidthInPx(this);
        float height = DensityUtil.dip2px(this, 200.f);
        mRlVv.getLayoutParams().height = (int) height;
        mRlVv.getLayoutParams().width = (int) width;
    }
}
複製程式碼

自定義工具類

//DensityUtil.java
public static final float getHeightInPx(Context context) {
	final float height = context.getResources().getDisplayMetrics().heightPixels;
	return height;
}
public static final float getWidthInPx(Context context) {
	final float width = context.getResources().getDisplayMetrics().widthPixels;
	return width;
}
複製程式碼

另外,如果是將播放器放於fragment中進行橫豎屏切換,則需要在onCreateView中setRetainInstance(true);,這樣旋轉後,才不會重新建立從頭開始播放;

獲取第一幀的內容作為封面

參考文章

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private void createVideoThumbnail() {
    Observable<Bitmap> observable = Observable.create(new Observable.OnSubscribe<Bitmap>() {
        @Override
        public void call(Subscriber<? super Bitmap> subscriber) {
            Bitmap bitmap = null;
            MediaMetadataRetriever retriever = new MediaMetadataRetriever();
            int kind = MediaStore.Video.Thumbnails.MINI_KIND;
            if (Build.VERSION.SDK_INT >= 14) {
                retriever.setDataSource(mVideoUrl, new HashMap<String, String>());
            } else {
                retriever.setDataSource(mVideoUrl);
            }
            bitmap = retriever.getFrameAtTime();
            subscriber.onNext(bitmap);
            retriever.release();
        }
    });

observable.observeOn(AndroidSchedulers.mainThread())
        .subscribeOn(Schedulers.io())
        .subscribe(new Action1<Bitmap>() {
            @Override
            public void call(Bitmap bitmap) {
                //設定封面
                mYourVideoPlayerContainer.setBackgroundDrawable(new BitmapDrawable(bitmap));
            }
        });
}
複製程式碼

滑動改變螢幕亮度/音量

  • 許可權申請
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.VIBRATE"/> //按需申請
複製程式碼
  • 修改亮度方法
/*設定當前螢幕亮度值 0--255,並使之生效*/
private void setScreenBrightness(float value) {
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    lp.screenBrightness = lp.screenBrightness + value / 255.0f;
    Vibrator vibrator;
    if (lp.screenBrightness > 1) {
        lp.screenBrightness = 1;
        //              vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
        //              long[] pattern = {10, 200}; // OFF/ON/OFF/ON...
        //              vibrator.vibrate(pattern, -1);
    } else if (lp.screenBrightness < 0.2) {
        lp.screenBrightness = (float) 0.2;
        //              vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
        //              long[] pattern = {10, 200}; // OFF/ON/OFF/ON...
        //              vibrator.vibrate(pattern, -1);
    }
    getWindow().setAttributes(lp);

    // 儲存設定的螢幕亮度值
    // Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, (int) value);
}
複製程式碼
  • 設定螢幕亮度模式方法 (自動/手動)
// value 可取值: Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC / SCREEN_BRIGHTNESS_MODE_MANUAL
private void setScreenMode(int value) {
    Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, value);
}
複製程式碼
  • 監聽播放區域
mGestureDetector = new GestureDetector(this, mGestureListener);
vv.setOnTouchListener(this); 
@Override
public boolean onTouch(View v, MotionEvent event) {
    return mGestureDetector.onTouchEvent(event);
}
複製程式碼
  • onScroll的時候動態改變亮度 onDown() / onScroll() 返回true
private android.view.GestureDetector.OnGestureListener mGestureListener = new GestureDetector.OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        final double FLING_MIN_VELOCITY = 0.5;
        final double FLING_MIN_DISTANCE = 0.5;

        if (e1.getY() - e2.getY() > FLING_MIN_DISTANCE
                && Math.abs(distanceY) > FLING_MIN_VELOCITY) {
            setScreenBrightness(20);
        }
        if (e1.getY() - e2.getY() < FLING_MIN_DISTANCE
                && Math.abs(distanceY) > FLING_MIN_VELOCITY) {
            setScreenBrightness(-20);
        }
        return true;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return true;
    }
};
複製程式碼

滑動修改音量

修改上方的 onScroll() 方法,呼叫以下操作

    private void setVoiceVolume(boolean volumeUp) {
        //  設定音量絕對值的話,我在小米上突破不了限制,最大音量15,但是設定到10的時候就沒法再增加了,最後使用系統的音量控制才可以
        //        int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
        //        int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        //        int flag = volumeUp ? 1 : -1;
        //        currentVolume += flag * 1;
        //        if (currentVolume >= maxVolume) {
        //            currentVolume = maxVolume;
        //        } else if (currentVolume <= 1) {
        //            currentVolume = 1;
        //        }
        //        Log.i(TAG, "setVoiceVolume currentVolume = " + currentVolume + " ,maxVolume = " + maxVolume);
        //        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0);

        //降低音量,調出系統音量控制
        if (volumeUp) {
            mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE,
                    AudioManager.FX_FOCUS_NAVIGATION_UP);
        } else {//增加音量,調出系統音量控制
            mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER,
                    AudioManager.FX_FOCUS_NAVIGATION_UP);
        }
    }
複製程式碼
  • 在頁面關閉時可考慮恢復亮度/音量初始值
  • 在onTouch的時候對觸點進行判斷,區分是修改音量或是改變亮度

需要處理的問題

拖動進度條,手動seekTo後,進度會跳動

斷點跟蹤後發現是native方法的問題,各大視訊播放平臺的客戶端,比較普遍存在,暫無法處理:

優酷客戶端時間跳變

找到些資源:

  1. 關於Android VideoView seekTo不準確的解決方案
  2. 視訊關鍵幀提取 第一個提到的關鍵幀問題,我找了個視訊測試了下,seekTo到固定的時間點,則跳變的位置也固定;

暫停/恢復 頁面時,視訊重新載入

現象: 在視訊播放時,使頁面 onPause() ,之後再恢復,則 videoView 會重新開始播放,臨時的處理方案是在 onPause() 的時候記錄當前播放進度位置,在 onResume() 的時候拖動到該進度位置,但是該方案仍會有黑屏現象,程式碼如下:

int mPlayingPos = 0;

@Override
protected void onPause() {
    mPlayingPos = mVideoView.getCurrentPosition(); //先獲取再stopPlay(),原因自己看原始碼
    mVideoView.stopPlayback();
    super.onPause();
}

@Override
protected void onResume() {
    if (mPlayingPos > 0) {
        //此處為更好的使用者體驗,可新增一個progressBar,有些客戶端會在這個過程中隱藏底下控制欄,這方法也不錯
        mVideoView.start();
        mVideoView.seekTo(mPlayingPos);
        mPlayingPos = 0;
    }
    super.onResume();
}
複製程式碼

找到些可能相關的文章,連結已失效,快照如下(還得去看看 surfaceView 啊 ~ ~# ): 另一篇類似的: android開發常見問題 問題7,也指明是 surfaceview 的原因,之所以是黑色的見後面的解釋:

Activity 呼叫的順序是 onPause() -> onStop() SurfaceView 呼叫了 surfaceDestroyed() 方法 然後再切回程式 Activity 呼叫的順序是 onRestart() -> onStart() -> onResume () SurfaceView` 呼叫了 surfaceChanged() -> surfaceCreated() 方法 按結束通話鍵或鎖定螢幕 Activity 只呼叫 onPause() 方法 解鎖後 Activity 呼叫 onResume() 方法 SurfaceView 什麼方法都不呼叫

網路變化/切換應用後恢復播放

播放過程中,假如只緩衝了一部分視訊,則當播放完緩衝部分後,會丟擲1004異常,即使此時網路連線已經恢復,控制元件也不會自動繼續緩衝: MediaPlayer: error (1, -1004) 原始碼註釋: File or network related operation errors 同時,由於SurfaceView在頁面onStop()時會destroy,比如播放時,使用者按下home鍵或切換到其他應用頁面再返回時,視訊播放停止,此時需要重新載入視訊並播放到上次停止的位置;

另外,有測試發現在三星G9200手機上,報 1004 這個錯的時候會彈出錯誤提示框,然後卡死重啟... 對比了下日誌:

// API23 MediaPlayer.java
@Override
public void handleMessage(Message msg) {
    if (mMediaPlayer.mNativeContext == 0) {
        Log.w(TAG, "mediaplayer went away with unhandled events");
        return;
    }
    switch(msg.what) {
        case MEDIA_ERROR:
        Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
        boolean error_was_handled = false;
        if (mOnErrorListener != null) {
            error_was_handled = mOnErrorListener.onError(mMediaPlayer, msg.arg1, msg.arg2);
        }
        if (mOnCompletionListener != null && ! error_was_handled) {
            mOnCompletionListener.onCompletion(mMediaPlayer);
        }
        stayAwake(false);
        return;
    default:
        Log.e(TAG, "Unknown message type " + msg.what);
        return;
    }
}
複製程式碼
//API23 VideoView.java
public void setOnErrorListener(OnErrorListener l){
    mOnErrorListener = l;
}


private MediaPlayer.OnErrorListener mErrorListener =
    new MediaPlayer.OnErrorListener() {
    public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
    ......

        /* If an error handler has been supplied, use it and finish. */
        if (mOnErrorListener != null) { //如果這裡沒有處理,則每次發生異常都會彈出提示框,可能造成崩潰
            if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
                return true;
            }
        }

        /* Otherwise, pop up an error dialog so the user knows that
            * something bad has happened. Only try and pop up the dialog
            * if we're attached to a window. When we're going away and no
            * longer have a window, don't bother showing the user an error.
            */
        if (getWindowToken() != null) {
            Resources r = mContext.getResources();
            int messageId;

            if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
                messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
            } else {
                messageId = com.android.internal.R.string.VideoView_error_text_unknown;
            }

            // 彈出錯誤提示框
            new AlertDialog.Builder(mContext)
                    .setMessage(messageId)
                    .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog, int whichButton) {
                                    /* If we get here, there is no onError listener, so
                                        * at least inform them that the video is over.
                                        */
                                    if (mOnCompletionListener != null) {
                                        mOnCompletionListener.onCompletion(mMediaPlayer);
                                    }
                                }
                            })
                    .setCancelable(false)
                    .show();
        }
        return true;
    }
};

複製程式碼

因此需要對網路變化進行監聽:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
複製程式碼
@Override
protected void onResume() {
    super.onResume();
    mNetworkState = NetworkHelper.getNetworkType(this);
    //播放網路視訊時,需要檢測判斷網路狀態變化
    if (SCHEME_HTTP.equalsIgnoreCase(mVideoUri.getScheme()) && mNetworkState == 0) {
        MessageUtils.showAlertDialog(this, "提示", getResources().getString(R.string.network_error), null);
    } else {
        if (mPlayingPos > 0) {
            mVv.start();
            mVv.seekTo(mPlayingPos);
            mPlayingPos = 0;
        }
    }
}

/**
 * 監聽網路變化,用於重新緩衝
 */
private void registerNetworkReceiver() {
    if (mNetworkReceiver == null) {
        mNetworkReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (SCHEME_HTTP.equalsIgnoreCase(mVideoUri.getScheme())
                            && action.equalsIgnoreCase(ConnectivityManager.CONNECTIVITY_ACTION)) {
                    doWhenNetworkChange();
                }
            }
        };
    }
    registerReceiver(mNetworkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}


/**
 * 網路播放
 */
public void doWhenNetworkChange() {
    mNetworkState = NetworkHelper.getNetworkType(this);
    //儲存當前已快取長度
    int bufferPercentage = mVv.getBufferPercentage();
    mLastLoadLength = bufferPercentage * mVv.getDuration() / 100;
    //這裡需要判斷 0
    int currentPosition = mVv.getCurrentPosition();
    if (currentPosition > 0) {
        mPlayingPos = currentPosition;
    }
    debugLog(bufferPercentage + " 網路變化 ... " + mNetworkState + " 快取長度 " + mLastLoadLength + " -- " + currentPosition);

    if (mNetworkState == NetworkHelper.NETWORK_TYPE_INVALID && bufferPercentage < 100) {
        // 監聽當前播放位置,在達到緩衝長度前自動停止
        if (mCheckPlayingProgressTimer == null) {
            mCheckPlayingProgressTimer = new Timer();
        }
        mCheckPlayingProgressTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (mPlayingPos >= mLastLoadLength - deltaTime) {
                    mVv.pause();
                }
            }
        }, 0, 1000);//每秒檢測一次
    } else {
        restartPlayVideo();
    }
}

private void restartPlayVideo() {
    //todo 新增 progressBar 體驗好點
    if (mCheckPlayingProgressTimer != null) {
        mCheckPlayingProgressTimer.cancel();
        mCheckPlayingProgressTimer = null;
    }
    mVv.setVideoURI(mVideoUri);
    mVv.start();
    mVv.seekTo(mPlayingPos);

    mLastLoadLength = -1;
    mPlayingPos = 0;
}

@Override
protected void onPause() {
    mPlayingPos = mVv.getCurrentPosition();
    mVv.pause();
    super.onPause();
}

@Override
protected void onStop() {
    mVv.stopPlayback();
    mLastLoadLength = 0;
    debugLog("onResume " + mPlayingPos + " -- " + mLastLoadLength);
    super.onStop();
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (mCheckPlayingProgressTimer != null) {
        mCheckPlayingProgressTimer.cancel();
        mCheckPlayingProgressTimer = null;
    }
    ......
    unregisterNetworkReceiver();
}
複製程式碼

seekbar變化超出緩衝長度

使用系統提供的控制元件 mVv.setMediaController(new MediaController(this)); 的話,在斷網時,仍可以拖動超出緩衝長度的範圍,會報錯,這個還是得自定義才能控制可拖動位置,不再贅述;

MediaPlayer: Attempt to perform seekTo in wrong state: mPlayer=0x7f7ebbf5c0, mCurrentState=0
MediaPlayer: Error (1,-1004)
複製程式碼
//API 23 MediaController.java
private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
        if (!fromuser) {
            // We're not interested in programmatically generated changes to
            // the progress bar's position.
            return;
        }

        //這裡就只是設定mediaplayer的播放位置而已
        long duration = mPlayer.getDuration();
        long newposition = (duration * progress) / 1000L;
        mPlayer.seekTo( (int) newposition);
        if (mCurrentTime != null)
            mCurrentTime.setText(stringForTime( (int) newposition));
    }
}
複製程式碼

當斷網後,使用者拖動超出緩衝區長度的話mediaplayer報錯,此時再次點選VideoView區域,不會觸發顯示控制條,真是各種不方便啊,還是建議自己寫一個控制條;

SurfaceView

資源

  1. SurfaceView 原始碼分析及使用 這篇講到了 SurfaceView 會顯示黑色區域的原因:

SurfaceView 的 draw 和 dispatchDraw 方法中看到,SurfaceView 中,windownType變數被初始化為WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,所以在建立繪製這個 View 的過程中整個 Canvas 會被塗成黑色

  1. 浮層視訊效果,在另外一個Window使用SurfaceView無法正常顯示的問題排查與解決

surfaceView黑屏

  1. 無內容時,預設會繪製黑色背景圖
//SurfaceView.java
int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
public void draw(Canvas canvas) {
    if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
        // draw() is not called when SKIP_DRAW is set
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
            // punch a whole in the view-hierarchy below us
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
    }
    super.draw(canvas);
}
複製程式碼

感謝@尚弟很忙噠 的提醒, 設定頁面主題為透明(android:theme="@android:style/Theme.Translucent")時,在初始緩衝階段,VideoView區域會變成透明:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="org.lynxz.androiddemos.VideoViewActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@android:color/holo_blue_bright">

        <VideoView
            android:id="@+id/vv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"/>
    </RelativeLayout>
</RelativeLayout>
複製程式碼

在SurfaceView類開頭有醬紫的一段註釋,大概能解釋為什麼緩衝時候視訊區域會透明瞭:

The surface is Z ordered so that it is behind the window holding its SurfaceView; 
the SurfaceView punches a hole in its window to allow its surface to be displayed.
複製程式碼

因此處理方案就變成將SurfaceView挪到上層即可:

mVv.setZOrderOnTop(true);
複製程式碼

不過挪動之後就可以設定VideoView的背景,此時才不會遮蓋實際的視訊繪圖了,xml中指定吧,這裡省略,不過如果VideoView區域還有其他控制元件的話,會被遮蓋,所以最後我就沒設定zorderOnTop了,而是直接在xml中指定VideoView的背景色,然後在onPrepare回撥的時候,去掉背景即可(按需延時,或者在有播放進度,要更新進度條的時候進行去掉背景操作都ok,不然可能會有一瞬間的透明):

mVv.setBackgroundColor(Color.TRANSPARENT);
複製程式碼

之前是打算像網上說的給VideoView的holder新增一個callback,(mVv.getHolder().addCallback(new SurfaceHolder.Callback() {...}) ,在 surfaceCreated() 的時候獲取canvas並手動繪製背景色,但是 holder.lockCanvas() 一直返回 null ,log資訊提示:

E/SurfaceHolder: Exception locking surface
                           java.lang.IllegalArgumentException
                           at android.view.Surface.nativeLockCanvas(Native Method)
                            .......
複製程式碼

看到native我暫時就沒招了,打住,老實用變通方法吧;

  1. 手機 "選單鍵" 導致應用被stop,雖然此時看起來可見 SurfaceView.java 的註釋: 在呼叫選單鍵的時候雖然頁面貌似可見,但實際已經呼叫了onStop()方法了,而surface在window不可見時會銷燬:

The Surface will be created for you while the SurfaceView's window is visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated} and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the Surface is created and destroyed as the window is shown and hidden.

按下選單鍵後返回

  1. VideoView無法播放f4v格式(三星s6可以播放,紅米1s(4.4.4)播放失敗).... 以後能力夠了可以參考下這篇 :

相關文章