Android 列表视频的全屏、自动小窗口优化实践

Hello,爱猫的老司机来埋坑啦<( ̄︶ ̄)>,鉴于之前的《Android 实现视屏播放器、边播边缓存功能、外加铲屎(IJKPlayer)》好像还挺多人关注的,文中一些地方因为篇幅(就是懒)问题一笔带过,这篇就拓扑聊一聊其中列表全屏,还有播放中的视频滑出屏幕用小窗口播放的实现,刚好最近有做了一些调整。

上例牌 github>>>>>>>> https://github.com/CarGuo 对,就是这个郭老司机。

本期就不话唠了,周一谁有精力说话呢┑( ̄Д  ̄)┍,程序猿的周末是什么?

列表中播放视频全屏展示

看过小喵上一篇视频相关文章的应该知道小喵手贱的用了两种实现方式,一种是基于懒人的系统层模式;一种是基于单例的UI逻辑播放器的模式的ListVideoUtil。至于为什么是两种呢?因为手贱啊。(ノಠ益ಠ)ノ彡┻━┻,本文如有不明之处可结合前文一起食用:《Android 实现视屏播放器、边播边缓存功能、外加铲屎(IJKPlayer)》

1、系统层实现全屏播放

伟人曾经说过,每一个Activity都有一个自己的默认布局,这里面又包含有了一个com.android.internal.R.id.content,而且是一个FrameLayout(请无视上面的废话),如此看来用来作为我们全屏显示的父布局妥妥的。此处手贱的加入了动画效果的支持,一直觉得5.0的过渡动画挺高大上的,作为一个material design的应用必须有这样的逼格(什么?你说兼容?这里美女太多我听不到····)。

作为一只内向的程序猿,语言组织能力有限,我们还是从代码上来,从代码上去吧,注释满满的,顺序看下去不难理解(前提是你看的下,确实长♂了点)。

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,就可以让视频小窗体在它的父布局内移动啦。

小窗口
/**
 * 显示小窗口
 */
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可以去呢:https://github.com/CarGuo

来一下嘛
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容