Android 列表視訊的全屏、自動小視窗優化實踐

戀貓de小郭發表於2019-03-01
 Hello,愛貓的老司機來埋坑啦<( ̄︶ ̄)>,鑑於之前的《Android 實現視屏播放器、邊播邊快取功能、外加鏟屎(IJKPlayer)》好像還挺多人關注的,文中一些地方因為篇幅(就是懶)問題一筆帶過,這篇就拓撲聊一聊其中列表全屏,還有播放中的視訊滑出螢幕用小視窗播放的實現,剛好最近有做了一些調整。

 
上例牌 github>>>>>>>> github.com/CarGuo 對,就是這個郭老司機。

本期就不話嘮了,週一誰有精力說話呢┑( ̄Д  ̄)┍,程式猿的週末是什麼?

列表中播放視訊全屏展示

 
 看過小喵上一篇視訊相關文章的應該知道小喵手賤的用了兩種實現方式,一種是基於懶人的系統層模式;一種是基於單例的UI邏輯播放器的模式的ListVideoUtil。至於為什麼是兩種呢?因為手賤啊。(ノಠ益ಠ)ノ彡┻━┻,本文如有不明之處可結合前文一起食用:《Android 實現視屏播放器、邊播邊快取功能、外加鏟屎(IJKPlayer)》

1、系統層實現全屏播放

 
 偉人曾經說過,每一個Activity都有一個自己的預設佈局,這裡面又包含有了一個com.android.internal.R.id.content,而且是一個FrameLayout(請無視上面的廢話),如此看來用來作為我們全屏顯示的父佈局妥妥的。此處手賤的加入了動畫效果的支援,一直覺得5.0的過渡動畫挺高大上的,作為一個material design的應用必須有這樣的逼格(什麼?你說相容?這裡美女太多我聽不到····)。

 作為一隻內向的程式猿,語言組織能力有限,我們還是從程式碼上來,從程式碼上去吧,註釋滿滿的,順序看下去不難理解(前提是你看的下,確實長♂了點)。

Android 列表視訊的全屏、自動小視窗優化實踐

1.1 進入全屏

 

  • 獲取到了com.android.internal.R.id.content這個ViewGroup。
  • 清除當前列表播放器L上的TextureView渲染控制元件,等待全屏播放器F的渲染控制元件。
  • 新建立一個視訊邏輯播放器F,為它設定一個固定id,這樣幹掉它的時候通過這個id也能快速找到。
  • 儲存當前的狀態列、標題欄資訊和列表中在螢幕位置的資訊,用於恢復到原本的狀態。
  • 建立一個黑色背景的FrameLayout,充滿螢幕用來承載全屏播放器F,這樣全屏播放器F可以在其中執行動畫效果。
  • 5.0以下直接加全屏播放器F到ViewGroup居中充滿全屏,5.0以上則執行動畫。
  • 5.0以上先通過margin讓全屏播放器加入到ViewGroup同列表的位置一致,之後通過過渡動畫平移到螢幕中間,居中充滿全屏。

怎麼樣,看起來是不是有些混亂?(ノಠ益ಠ)ノ彡┻━┻,我就說程式猿還是看程式碼好溝通是吧,雖然很長就是。

 
//獲得com.android.internal.R.id.content
private ViewGroup getViewGroup() {    
    return (ViewGroup) (CommonUtil.scanForActivity(getContext())).findViewById(Window.ID_ANDROID_CONTENT);
}

···此處省略無數只草泥馬

//這兩個是TextureView的回撥,在這remove和onPause還有add的時候基本會進入
 @Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    //更新資料到這個surface上渲染
    mSurface = new Surface(surface);
    GSYVideoManager.instance().setDisplay(mSurface);
}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    //告訴資料管理器這個渲染控制元件放棄了
    GSYVideoManager.instance().setDisplay(null);
    surface.release();
    return true;
}

···此處省略無數只草泥馬

這個開始全屏的頁面邏輯

//將播放的視訊渲染控制元件移除,進入上面的回撥,讓新的邏輯播放器可以接入
if (mTextureViewContainer.getChildCount() > 0) {
    mTextureViewContainer.removeAllViews();
}

//儲存全屏之前的狀態列和
saveLocationStatus(context, statusBar, actionBar);

try {
    //生成一個播放器,因為繼承關係,會建立一個當前列表item一樣的UI邏輯播放器
    //這些邏輯都是寫在GSYBaseVideoPlayer這個抽象類下
    Constructor<GSYBaseVideoPlayer> constructor = (Constructor<GSYBaseVideoPlayer>) GSYBaseVideoPlayer.this.getClass().getConstructor(Context.class);
    final GSYBaseVideoPlayer gsyVideoPlayer = constructor.newInstance(getContext());
    //給它一個固定的id,在這樣移除的時候就知道在哪裡
    gsyVideoPlayer.setId(FULLSCREEN_ID);
    //獲取螢幕的高度
    WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    final int w = wm.getDefaultDisplay().getWidth();
    final int h = wm.getDefaultDisplay().getHeight();
    //建立一個層用於加入都window層中,設定為黑色,用於包含著播放器
    FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    FrameLayout frameLayout = new FrameLayout(context);
    frameLayout.setBackgroundColor(Color.BLACK);
    //如果5.0的機器就執行動畫,這裡其實可以用VauleAnimaton相容5.0以下的
    if (mShowFullAnimation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        //先把播放器的位置設定為在列表中一樣位置
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight());
        lp.setMargins(mListItemRect[0], mListItemRect[1], 0, 0);
        frameLayout.addView(gsyVideoPlayer, lp);
        vp.addView(frameLayout, lpParent);
        //稍微延時執行動畫
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //開啟5.0動畫
                TransitionManager.beginDelayedTransition(vp);
                //將播放器跳轉為充滿居中,系統自動過渡
                resolveFullVideoShow(context, gsyVideoPlayer, h, w);
            }
        }, 300);
    } else {
        //非5.0的直接將播放器的佈局加入到佈局下
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight());
        frameLayout.addView(gsyVideoPlayer, lp);
        vp.addView(frameLayout, lpParent);
        //將播放器跳轉為充滿居中
        resolveFullVideoShow(context, gsyVideoPlayer, h, w);
    }
    //設定全屏邏輯播放器和當前列表的邏輯狀態一致
    gsyVideoPlayer.setUp(mUrl, mCache, mObjects);
    gsyVideoPlayer.setStateAndUi(mCurrentState);
    //新增上渲染控制元件,通知資料載入管理器是用這個渲染
    gsyVideoPlayer.addTextureView();
    //配置對應UI
    gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_shrink);
    gsyVideoPlayer.getFullscreenButton().setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            clearFullscreenLayout();
        }
    });

    gsyVideoPlayer.getBackButton().setVisibility(VISIBLE);
    gsyVideoPlayer.getBackButton().setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            clearFullscreenLayout();
        }
    });
    //將資料載入管理器的介面回到配置到全屏播放器裡面
    GSYVideoManager.instance().setLastListener(this);
    GSYVideoManager.instance().setListener(gsyVideoPlayer);

} catch (Exception e) {
    e.printStackTrace();
}

···此處省略無數只草泥馬

/**
 * 全屏
 */
private void resolveFullVideoShow(Context context, GSYBaseVideoPlayer gsyVideoPlayer) {
    //清除動畫的margin
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) gsyVideoPlayer.getLayoutParams();
    lp.setMargins(0, 0, 0, 0);
    //居中充滿
    lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
    lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
    lp.gravity = Gravity.CENTER;
    gsyVideoPlayer.setLayoutParams(lp);
    gsyVideoPlayer.setIfCurrentIsFullscreen(true);
    //加入旋轉工具類
    mOrientationUtils = new OrientationUtils((Activity) context, gsyVideoPlayer);
    mOrientationUtils.setEnable(mRotateViewAuto);
}複製程式碼
1.2 退出全屏

 
 既然都進去了♂,出來還難嗎?所以我們只需要反著來就行了,下面直接長程式碼,有註釋。(男人長一點有什麼錯┑( ̄Д  ̄)┍)

  • 是否橫屏,是的話先轉為豎屏
  • 恢復狀態列和標題欄
  • 5.0以下直接清除當前列全屏播放器F,恢復視訊狀態
  • 5.0以上顯示讓全屏播放器F過渡到原本的位置,再清除恢復視訊狀態
/**
 * 退出系統層播放全屏效果
 */
public void clearFullscreenLayout() {

    //需要判斷當前是否橫屏,是的話要轉為介面之後稍等一會在退回,這樣才不會介面抖動
    int delay = mOrientationUtils.backToProtVideo();
    //關閉旋轉
    mOrientationUtils.setEnable(false);
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            backToNormal();
        }
    }, delay);
}

/**
 * 回到正常效果
 */
private void backToNormal() {
    //恢復狀態
    showSupportActionBar(mContext, mActionBar, mStatusBar);
    final ViewGroup vp = getViewGroup();
    //拿到content和播放器
    final View oldF = vp.findViewById(FULLSCREEN_ID);
    final GSYVideoPlayer gsyVideoPlayer;
    if (oldF != null) {
        gsyVideoPlayer = (GSYVideoPlayer) oldF;
        if (mShowFullAnimation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            TransitionManager.beginDelayedTransition(vp);
            //執行動畫回到原本的列表中的位置
            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) gsyVideoPlayer.getLayoutParams();
            lp.setMargins(mListItemRect[0], mListItemRect[1], 0, 0);
            lp.width = mListItemSize[0];
            lp.height = mListItemSize[1];
            //注意配置回來,不然動畫效果會不對
            lp.gravity = Gravity.NO_GRAVITY;
            gsyVideoPlayer.setLayoutParams(lp);

            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    resolveNormalVideoShow(oldF, vp, gsyVideoPlayer);
                }
            }, 400);
        } else {
            //直接移除
            resolveNormalVideoShow(oldF, vp, gsyVideoPlayer);
        }

    } else {
        //直接移除
        resolveNormalVideoShow(null, vp, null);
    }
}


/**
 * 恢復
 */
private void resolveNormalVideoShow(View oldF, ViewGroup vp, GSYVideoPlayer gsyVideoPlayer) {
    //移除全屏播放器
    if (oldF.getParent() != null) {
        ViewGroup viewGroup = (ViewGroup) oldF.getParent();
        vp.removeView(viewGroup);
    }
    //拿回狀態
    mCurrentState = GSYVideoManager.instance().getLastState();
    if (gsyVideoPlayer != null) {
        mCurrentState = gsyVideoPlayer.getCurrentState();
    }
    //重新設定回撥
    GSYVideoManager.instance().setListener(GSYVideoManager.instance().lastListener());
    GSYVideoManager.instance().setLastListener(null);
    //播放器恢復
    setStateAndUi(mCurrentState);
    //通知資料載入播放器用回列表的渲染
    addTextureView();
    CLICK_QUIT_FULLSCREEN_TIME = System.currentTimeMillis();
}複製程式碼

2、ListVideoUtil實現全屏播放

 
 總體上邏輯和上文是一致的,只是這種實現在列表中是不包含邏輯播放器,邏輯播放器和全屏邏輯播放器都是一個單例,需要你手動在list列表的最外層加多一個佈局做全屏播放,在每個item那裡預留一個位置用於包容列表的播放器,還有一個播放按鈕用於播放。

 感覺很麻煩是吧,耦合度又高,但是它可以在視訊滑出介面的時候不被釋放,一直保持在原來的位置。

2.1 全屏

 和上面的邏輯基本一致,就不廢話了(可以偷懶了),只需要注意用的時候操作方式不一樣,總結起來就是有些麻煩。


//配置好全屏佈局
listVideoUtil.setFullViewContainer(videoFullContainer);
listVideoUtil.setHideStatusBar(true);

···此處省略無數只草泥馬

//增加封面
ImageView imageView = new ImageView(context);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setImageResource(R.mipmap.xxx1);
//將列表的位置,封面,列表的TAG,列表是的父佈局,播放按鍵傳入進去
listVideoUtil.addVideoPlayer(position, imageView, TAG, holder.videoContainer, holder.playerBtn);

holder.playerBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //更新其他item
        notifyDataSetChanged();
        //設定播放器的標誌位,防止錯位
        listVideoUtil.setPlayPositionAndTag(position, TAG);
        //url開始播放
        final String url = "http://baobab.wdjcdn.com/14564977406580.mp4";
        listVideoUtil.startPlay(url);
    }
});複製程式碼

列表中播放視訊小視窗播放

 
 有時候我們會想要視訊滑出螢幕的時候有個小視窗在右下角,最好還是可以關閉和拖動的(看視訊的時候可以快速最小化收起來,不停止,避免尷尬對吧)。邏輯和實現全屏一樣,用系統的content層來承載,不同的是利用margin讓視訊出現在右下角,這樣我們拖動的時候只要改變視訊的margin,就可以讓視訊小窗體在它的父佈局內移動啦。

Android 列表視訊的全屏、自動小視窗優化實踐
小視窗

/**
 * 顯示小視窗
 */
public void showSmallVideo(Point size, final boolean actionBar, final boolean statusBar) {
    //利用content實現,和全屏一樣,只是大小和背景色不一樣
    final ViewGroup vp = getViewGroup();

    removeVideo(vp, SMALL_ID);

    if (mTextureViewContainer.getChildCount() > 0) {
        mTextureViewContainer.removeAllViews();
    }

    try {
        Constructor<GSYBaseVideoPlayer> constructor = (Constructor<GSYBaseVideoPlayer>) GSYBaseVideoPlayer.this.getClass().getConstructor(Context.class);
        GSYBaseVideoPlayer gsyVideoPlayer = constructor.newInstance(getContext());
        gsyVideoPlayer.setId(SMALL_ID);

        FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        FrameLayout frameLayout = new FrameLayout(mContext);

        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(size.x, size.y);
        int marginLeft = CommonUtil.getScreenWidth(mContext) - size.x;
        int marginTop = CommonUtil.getScreenHeight(mContext) - size.y;

        if (actionBar) {
            marginTop = marginTop - getActionBarHeight((Activity) mContext);
        }

        if (statusBar) {
            marginTop = marginTop - getStatusBarHeight(mContext);
        }
        //利用margin讓視訊出現在右下角,這樣我們拖動的時候只要改變margin就好啦
        lp.setMargins(marginLeft, marginTop, 0, 0);
        frameLayout.addView(gsyVideoPlayer, lp);

        vp.addView(frameLayout, lpParent);
        //繼續播放
        gsyVideoPlayer.setUp(mUrl, mCache, mObjects);
        gsyVideoPlayer.setStateAndUi(mCurrentState);
        gsyVideoPlayer.addTextureView();
        gsyVideoPlayer.onClickUiToggle();
        gsyVideoPlayer.setSmallVideoTextureView(new SmallVideoTouch(gsyVideoPlayer, marginLeft, marginTop));

        GSYVideoManager.instance().setLastListener(this);
        GSYVideoManager.instance().setListener(gsyVideoPlayer);

    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }

}

/**
 * 隱藏小視窗
 */
public void hideSmallVideo() {
    final ViewGroup vp = getViewGroup();
    GSYVideoPlayer gsyVideoPlayer = (GSYVideoPlayer) vp.findViewById(SMALL_ID);
    removeVideo(vp, SMALL_ID);
    mCurrentState = GSYVideoManager.instance().getLastState();
    if (gsyVideoPlayer != null) {
        mCurrentState = gsyVideoPlayer.getCurrentState();
    }
    GSYVideoManager.instance().setListener(GSYVideoManager.instance().lastListener());
    GSYVideoManager.instance().setLastListener(null);
    setStateAndUi(mCurrentState);
    addTextureView();
    CLICK_QUIT_FULLSCREEN_TIME = System.currentTimeMillis();
}複製程式碼

 這是觸控邏輯,這拖動的視訊窗體的時候,通過改變margin來實現窗體的移動,注意不要跑飛了就要,加個閾值。多說無益,看程式碼(又省下了好多字):

public class SmallVideoTouch implements View.OnTouchListener {

    private int mDownX, mDownY;
    private int mMarginLeft, mMarginTop;
    private int _xDelta, _yDelta;
    private GSYBaseVideoPlayer mGsyBaseVideoPlayer;


    public SmallVideoTouch(GSYBaseVideoPlayer gsyBaseVideoPlayer, int marginLeft,  int marginTop) {
        super();
        mMarginLeft = marginLeft;
        mMarginTop = marginTop;
        mGsyBaseVideoPlayer = gsyBaseVideoPlayer;
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        final int X = (int) event.getRawX();
        final int Y = (int) event.getRawY();
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                mDownX = X;
                mDownY = Y;

                FrameLayout.LayoutParams lParams = (FrameLayout.LayoutParams) mGsyBaseVideoPlayer
                        .getLayoutParams();
                _xDelta = X - lParams.leftMargin;
                _yDelta = Y - lParams.topMargin;

                break;
            case MotionEvent.ACTION_UP:
                if (Math.abs(mDownY - Y) < 5 && Math.abs(mDownX - X) < 5) {
                    return false;
                } else {
                    return true;
                }
            case MotionEvent.ACTION_MOVE:
                FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mGsyBaseVideoPlayer
                        .getLayoutParams();

                layoutParams.leftMargin = X - _xDelta;
                layoutParams.topMargin = Y - _yDelta;

                //不能超過螢幕上下左右的位置
                if (layoutParams.leftMargin >= mMarginLeft) {
                    layoutParams.leftMargin = mMarginLeft;
                }

                if (layoutParams.topMargin >= mMarginTop) {
                    layoutParams.topMargin = mMarginTop;
                }

                if (layoutParams.leftMargin <= 0) {
                    layoutParams.leftMargin = 0;
                }

                if (layoutParams.topMargin <= 0) {
                    layoutParams.topMargin = 0;
                }

                mGsyBaseVideoPlayer.setLayoutParams(layoutParams);

        }
        return false;
    }

}複製程式碼

最後

   
 如果你看到這裡,恭喜你看完了<( ̄︶ ̄)>!那麼,下面還有沙發,請問您要坐一坐嗎?d=====( ̄▽ ̄*)b不坐也沒關係,還有github可以去呢:github.com/CarGuo

Android 列表視訊的全屏、自動小視窗優化實踐
來一下嘛

相關文章