本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布android
上一篇文章简单讲解了腾讯新闻的视频无缝切换效果的实现(视频在播放中进行页面切换),若是你没有看过上篇,能够先去看看Android 高仿腾讯新闻视频切换效果。 上一篇写得比较随意,只是讲解了两个页面间如何实现视频在播放中的切换(切换播放器的container)及滚动中止播放等,部分效果没有实现,有一些细节不是处理得很好,因此从新补上一篇更加详细的教程。相同的内容此次就不在赘述了。 一样,仍是先上效果图 git
此次播放器换成了JZVideoPlayer,若是项目中尚未接入播放器或者刚接入的,仍是建议换成PlayerBase,高度解耦,可扩展性高,提供无缝续播助手。github
JZVideoPlayer版本是以前的,而且改动有点大。这里主要介绍思路,跟注意点,用PlayerBase一样也能够实现的。 JZVideoPlayer实现无缝切换其实就是更改player的ViewParentbash
public void attachToContainer(ViewGroup container) {
detachSuperContainer();
if (container != null) {
container.addView(JZVideoPlayerManager.getCurrentJzvd(), new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
playerContainer = container;
}
}
public void detachSuperContainer() {
JZVideoPlayer player = JZVideoPlayerManager.getCurrentJzvd();
ViewParent parent = player.getParent();
if (parent != null && parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(player);
}
}
复制代码
4G跟wifi切换出现提示:注册一个广播进行监听微信
@Override
protected void onResume() {
super.onResume();
JZVideoPlayer.goOnPlayOnResume();
IntentFilter filter = new IntentFilter();
filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
registerReceiver(wifiReceiver, filter);
}
@Override
protected void onStop() {
super.onStop();
try {
//weChat moment share will execute twice so try catch
unregisterReceiver(wifiReceiver);
} catch (Exception e) {
e.printStackTrace();
}
}
private BroadcastReceiver wifiReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) {
NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
if (info != null) {
if (info.getState().equals(NetworkInfo.State.DISCONNECTED)) {
if (JZMediaManager.isWiFi) {
JZMediaManager.isWiFi = false;
JZVideoPlayer.WIFI_TIP_DIALOG_SHOWED = false;
if(播放中或者加载中){
JZMediaManager.instance().jzMediaInterface.pause();
JZVideoPlayerManager.getCurrentJzvd().onStatePause();
}
}
} else if (info.getState().equals(NetworkInfo.State.CONNECTED)) {
if (!JZMediaManager.isWiFi) {
JZMediaManager.isWiFi = true;
JZVideoPlayer.WIFI_TIP_DIALOG_SHOWED = true;
if (JZVideoPlayerManager.getCurrentJzvd() != null &&
JZVideoPlayerManager.getCurrentJzvd().currentState == JZVideoPlayer.CURRENT_STATE_PAUSE) {
JZVideoPlayer.goOnPlayOnResume();
}
}
}
}
}
}
};
复制代码
这里放在onResume里面注册是由于个人项目不止一个页面有视频,因此须要在这里监听。这里注意一下,微信分享的时候onStop会调用2次,因此要try catch。4G切wifi的时候,要注意若是是用户手动暂停,是不须要自动播放的。app
public static void onScrollPlayVideo(RecyclerView recyclerView, int firstVisiblePosition, int lastVisiblePosition) {
if (JZMediaManager.isWiFi) {
for (int i = 0; i <= lastVisiblePosition - firstVisiblePosition; i++) {
View child = recyclerView.getChildAt(i);
View view = child.findViewById(R.id.player);
if (view != null && view instanceof JZVideoPlayerStandard) {
JZVideoPlayerStandard player = (JZVideoPlayerStandard) view;
if (getViewVisiblePercent(player) == 1f) {
if (JZMediaManager.instance().positionInList != i + firstVisiblePosition) {
player.startButton.performClick();
}
break;
}
}
}
}
}
复制代码
这里使用的是播放中item的position去判断是不是第一个彻底可见的视频,若是你的item的position会变(别问我为何,真的会有这种状况,手动狗头),就要用ide
JZVideoPlayerManager.getCurrentJzvd() != player
复制代码
去判断。 计算view的可见百分比,范围是0-1布局
public static float getViewVisiblePercent(View view) {
if (view == null) {
return 0f;
}
float height = view.getHeight();
Rect rect = new Rect();
if (!view.getLocalVisibleRect(rect)) {
return 0f;
}
float visibleHeight = rect.bottom - rect.top;
Log.d(TAG, "getViewVisiblePercent: emm " + visibleHeight);
return visibleHeight / height;
}
复制代码
public static void onScrollReleaseAllVideos(int firstVisiblePosition, int lastVisiblePosition,float percent) {
int currentPlayPosition = JZMediaManager.instance().positionInList;
if (currentPlayPosition >= 0) {
if ((currentPlayPosition <= firstVisiblePosition || currentPlayPosition >= lastVisiblePosition - 1)) {
if (getViewVisiblePercent(JZVideoPlayerManager.getCurrentJzvd()) < percent) {
JZVideoPlayer.releaseAllVideos();
}
}
}
}
复制代码
//初版
holder.itemView.setTranslationY(attr.getY() - l[1]);
holder.container.setScaleX(attr.getWidth() / (float) holder.container.getMeasuredWidth());
holder.container.setScaleY(attr.getHeight() / (float) holder.container.getMeasuredHeight());
//修改版
holder.itemView.setTranslationY(attr.getY() - l[1] - (holder.container.getMeasuredHeight() - attr.getHeight()) / 2);
holder.container.setScaleX(attr.getWidth() / (float) holder.container.getMeasuredWidth());
holder.container.setScaleY(attr.getHeight() / (float) holder.container.getMeasuredHeight());
复制代码
若是容器大小相同(视频列表页进入评论页),那直接用坐标相减就行,但这里对播放器的大小进行了改变,就须要减去高度差的一半,这里还要除以2是由于缩放的中心是view的中点。post
这里因为用的JZVideoPlayer,须要固定播放容器的宽高,否则会触发view的onMeasure致使闪烁动画
进入这个页面的时候须要分直接进入和视频播放进入两种状况。直接进入,就是直接添加fragment,再播放第一个视频,视频播放进入就是无缝切换效果。退出页面同理
PS:无缝切换的时候要留意一下,在新闻页,点击是直接进入视频列表,而在视频列表这里,点击是出现控制器的。在新闻页有个倒计时动画,而在视频列表页是没有的。这些在页面切换的时候,都须要进行对应的显示隐藏和点击事件的设置等等。
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy != 0) {
JZUtils.onScrollReleaseAllVideos(mLayoutManager.findFirstVisibleItemPosition(), mLayoutManager.findLastVisibleItemPosition(), 0.2f);
}
}
复制代码
这里的onScrolled()
方法有个小小的坑(我的感受)。
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
* <p>
* This callback will also be called if visible item range changes after a layout
* calculation. In that case, dx and dy will be 0.
*
* @param recyclerView The RecyclerView which scrolled.
* @param dx The amount of horizontal scroll.
* @param dy The amount of vertical scroll.
*/
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
复制代码
当recyclerView滑动后,这个方法就会被回调。很正常对吧。但是下面还有两行呢。当可见item从新测量,布局后,也会触发这个方法,此时dx,dy都是0。这里要注意的就是咱们这里是有全屏功能的,并且还会切换横竖屏,那就会触发这个方法。致使功能不正常了。因此上面加了个不为0的判断。
播放完毕自动播放下一个视频 这里须要留意一下,播放下一个视频我是经过滑动下一个视频到顶部从而触发播放的。但是也会有这种状况,就是你的视频特别少(咱们的app就是),那就没法播放最后一个视频了。腾讯的数据量够大,通常不会有这个问题。因此当没有更多的时候,须要在recyclerView的底部插入一条数据,显示没有更多数据,就能够播放这个视频了,腾讯也是这么处理的,看得出来设计得很周全,一个页面只能彻底显示一个视频,考虑得十分全面啊。
遮罩
遮罩这里用的是自定义view,画一个半通明的背景。
当列表播放时,显示遮罩,而且须要一个过渡的效果(透明度动画)。
当滑动界面,显示评论页,切换全屏,退出视频列表时,隐藏遮罩,不须要过渡效果。
onVideoSizeChanged()
方法进行监听。if (JZMediaManager.instance().currentVideoWidth > JZMediaManager.instance().currentVideoHeight) {
JZVideoPlayer.FULLSCREEN_ORIENTATION = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
if(JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN){
JZUtils.setRequestedOrientation(JZVideoPlayerManager.getCurrentJzvd().getContext(), ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
} else {
JZVideoPlayer.FULLSCREEN_ORIENTATION = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
if(JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN){
JZUtils.setRequestedOrientation(JZVideoPlayerManager.getCurrentJzvd().getContext(), ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
复制代码
根据宽高设置进入全屏是竖屏仍是横屏(若是大家公司非主流,不能根据视频宽高判断,那就后台加个字段设置吧)。
android P这里有个bug,切换屏幕方向的时候会黑屏,暂时未发现解决办法,知道怎么解决的大佬欢迎下方留言啊!!!另外,部分国产手机seekbar点击以后不是直接跳到对应的进度,而是快进一点点,也是服了呀...
public void changeUrl(String url, Object... objects) {
this.currentUrlMapIndex = 0;
this.seekToInAdvance = 0;
LinkedHashMap map = new LinkedHashMap();
map.put(URL_KEY_DEFAULT, url);
Object[] dataSourceObjects = new Object[1];
dataSourceObjects[0] = map;
this.dataSourceObjects = dataSourceObjects;
this.objects = objects;
setState(CURRENT_STATE_PREPARING_CHANGING_URL);
resetProgressAndTime();
}
复制代码
主要就是重置一些状态,改变变量的值。 判断方向 一样也会触发onVideoSizeChanged()
方法,在里面进行判断就行了,其实就是上面那段代码啦。
PS:切换url的时候最好把画面渲染层隐藏起来,播放的时候再显示。否则的话部分机器可能会出现最后一帧的画面被拉伸的状况。
列表滑动
这里须要注意一下,咱们上面对滑动进行了监听,不能调用smoothScrollTo()或者smoothScrollBy()方法。这里能够直接调用scrollToPositionWithOffset(),直接滑动到对应位置(若是你不是LinearLayoutManager,那就本身想办法吧。)
若是你跟我同样,都是用的JZVideoPlayer,那下面就要留意一下啦
if (JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN) {
JZMediaManager.instance().positionInList++;
JZVideoPlayerManager.getCurrentJzvd().changeUrl(mList.get(JZMediaManager.instance().positionInList).getVideoUrl());
mLayoutManager.scrollToPositionWithOffset(JZMediaManager.instance().positionInList, 0);
mRecycler.postDelayed(new Runnable() {
@Override
public void run() {
JZVideoPlayerManager.setFirstFloor((JZVideoPlayer) mRecycler.getChildAt(0).findViewById(R.id.player));
}
}, 500);
}
复制代码
进入和退出视频列表页进行无缝播放时,对播放器的父view进行了更改,也就会须要进行addView或者removeView,而且修改相关接口等等操做。
if (还在播放第一个视频) {
videoListFragment.removeVideoList();
recycler.postDelayed(new Runnable() {
@Override
public void run() {
JZMediaManager.instance().positionInList = clickPosition;
int first = mLayoutManager.findFirstVisibleItemPosition();
View v = recycler.getChildAt(clickPosition - first);
if (v != null) {
final PlayerContainer container = v.findViewById(R.id.adapter_video_container);
if (不是无缝播放进入视频列表页) {
container.removeAllViews();
}
//播放器接口,状态设置
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.remove(videoListFragment);
transaction.commitAllowingStateLoss();
}
}, 800);
} else {
JZVideoPlayer.releaseAllVideos();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.remove(videoListFragment);
transaction.commitAllowingStateLoss();
if (是无缝播放进入视频列表页) {
int first = mLayoutManager.findFirstVisibleItemPosition();
View v = recycler.getChildAt(clickPosition - first);
if (v != null) {
final PlayerContainer container = v.findViewById(R.id.adapter_video_container);
container.removeAllViews();
//从新添加播放器
}
}
}
复制代码
仍是解释一下吧,这里分4种状况:
逻辑确实复杂,须要多看几遍
若是还没接入播放器,仍是用PlayerBase吧。
评论页跟上一次没有大的区别,作了一点小小的改动:视频播放完毕后,会重置为普通状态,退出评论页返回视频列表页,会自动播放下一条。代码就不贴了,详见demo。 大功告成,喝杯82年雪碧庆祝一下吧。
下面是关于动态加载ijkplayer so文件的,不须要的能够跳过 动态加载so目前只见到这2种方案:
File dir = getDir("libs", Context.MODE_PRIVATE);
File soFile = new File(dir, "ijkffmpeg.so");
复制代码
是soFile的路径。 而后就是加载so库,刚开始我觉得直接把IjkMediaPlayer.Java拷出来修改加载路径就大功告成,但是却仍是报错,找不到方法。查了下,发现JNI的方法名是须要包名+类名+方法名,而我这里直接拷过来,包名变了,也就找不到方法了。因此 须要把ijk整个库拷下来,引入到项目里再进行修改(也能够修改so库中的包名)。 PS:若是你担忧仍是找不到so,能够这样作
try {
jzMediaInterface.prepare();
} catch (Throwable e) {
e.printStackTrace();
Object dataSource = JZMediaManager.getCurrentDataSource();
Log.e(TAG, "handleMessage: " + e.getMessage());
Toast.makeText(MyApplication.getInstance(), "so error", Toast.LENGTH_SHORT).show();
JZVideoPlayer.setMediaInterface(new JZExoPlayer());
jzMediaInterface.currentDataSource = dataSource;
jzMediaInterface.prepare();
}
复制代码
捕获初始化错误,再切换回备用内核。
拖了很久终于把这个东西写完了,高难度的东西没多少,全都是细节的处理。虽然效果还能够,但仍是逃不了上次说的问题,不能在activity间切换,逻辑复杂,耦合度过高。
最后,附上源码,有问题或者有更好的实现方式,欢迎下方留言,有空看到会回复的。