仿网易/QQ空间视频列表滚动连播炫酷效果(V1.0 挖坑之路)
原创 2017-07-27 认真的 小苏

视频列表滚动连播技术探究系列
1、仿网易/QQ空间视频列表滚动连播炫酷效果(V1.0 挖坑之路)。
2、仿网易/QQ空间视频列表滚动连播炫酷效果(V2.0 填坑之路) 想看源码的,看这篇文章。
3、仿网易视频列表滚动连播炫酷效果(v3.0 稳定版-思想改变及优化) 稳定版-进行优化和思想上的改变。
4、RecyclerView 平滑滚动可控制滚动速度的终极解决方案。
5、仿网易视频列表连播炫酷效果 - v3.1 升级版-细节优化(网络状态切换、item点击事件等)
持续更新中.....
技术前沿
万众瞩目Instant Apps终于全面问世啦 感兴趣的同学去看一下吧!
来个段子解解压
有个精神病到了银行,用手敲了敲柜台玻璃,问柜员:这是防弹玻璃吗?柜员:是的。精神病:能防得住炸弹吗?柜员吓得脸色苍白,说:不能!精神病从兜里掏出一对大小王贴在玻璃上说:“炸”!!!二十秒后,只见柜员怯怯的说出3个字。。。精神病满意的走了!
仿网易视频列表滚动连播炫酷效果
先看效果图,再说实现思想

实现思路
首先分析功能:1、滚动时不播放,但是要亮起,当前屏幕内,item view显示百分比最大的一个。
2、停止滚动且手指抬起时自动播放。
3、播放完当前的视频,自动滚动到下一个并自动播放。
4、正在播放的当前视频,快要播放完毕时,弹出TextView提示播放下一个,点击TextView自动滚动到下一个。
以上就是我们要实现的功能点。
分析功能点
- 滚动时不播放,但是要亮起,当前屏幕内,item view显示百分比最大的一个
分析: 看到滚动两个字,首要想起的是滚动监听(这里使用RecyclerView) addOnScrollListener,我们要在列表滚动时,计算屏幕内item 的数量,且找出item view 显示的百分比,进行比较,若其中一个item 百分比为:100 或百分比占最大,我们亮起这个item,其他的item是暗色的。
ok,顺着这个思路,往下走,如何获取一个view的百分比呢?
这是我从网上搜到的,这样我们就可以获取一个view他在当前屏幕内所占的百分比了。
private final Rect mCurrentViewRect = new Rect();
public int getVisibilityPercents(View view) {
int percents = 100;
view.getLocalVisibleRect(mCurrentViewRect);
int height = view.getHeight();
if (viewIsPartiallyHiddenTop()) {
// view is partially hidden behind the top edge
percents = (height - mCurrentViewRect.top) * 100 / height;
} else if (viewIsPartiallyHiddenBottom(height)) {
percents = mCurrentViewRect.bottom * 100 / height;
}
return percents;
}
private boolean viewIsPartiallyHiddenBottom(int height) {
return mCurrentViewRect.bottom > 0 && mCurrentViewRect.bottom < height;
}
private boolean viewIsPartiallyHiddenTop() {
return mCurrentViewRect.top > 0;
}
既然拿到了百分比,就可以在recyclerview 的滚动监听中进行计算了,看下面代码
rl_video.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE://停止滑动
mScrollState = false;
//滑动停止和松开手指时,调用此方法 进行播放
aoutPlayVideo(recyclerView);
break;
case RecyclerView.SCROLL_STATE_DRAGGING://用户用手指滚动
mScrollState = true;
break;
case RecyclerView.SCROLL_STATE_SETTLING://自动滚动
mScrollState = true;
break;
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
//获取最后一个可见view的位置
lastItemPosition = linearManager.findLastVisibleItemPosition();
//获取第一个可见view的位置
firstItemPosition = linearManager.findFirstVisibleItemPosition();
//获取可见view的总数
visibleItemCount = linearManager.getChildCount();
if (mScrollState) { //滚动
srcollVisible(recyclerView, firstItemPosition, lastItemPosition, visibleItemCount);
} else { //停止 第一次进入时调用此方法,进行播放
aoutPlayVideo(recyclerView);
}
}
}
});
很简单的逻辑,同理我们的功能2 也就能实现了
-
停止滚动且手指抬起时自动播放。
什么时候回调这个状态呢 case RecyclerView.SCROLL_STATE_IDLE,可以打个log 测试下,当列表停止滑动且手指抬起时,就会调这个状态。那我们就可以在这个状态里,实现播放视频的逻辑了。 -
如何计算百分比,实现思路非常简单,简单的for循环而已,具体的都写有注释。
/**
* 滚动时 判断哪个view 显示百分比最大,哪个最大 视图亮起
*
* @param recyclerView
* @param firstItemPosition
* @param lastItemPosition
* @param visibleItemCount 屏幕显示的item数量
*/
private void srcollVisible(RecyclerView recyclerView, int firstItemPosition, int lastItemPosition, int visibleItemCount) {
for (int i = 0; i < visibleItemCount; i++) {
if (recyclerView != null) {
View childAt = recyclerView.getChildAt(i).findViewById(R.id.visiabile);
recyclerView.getChildAt(i).findViewById(R.id.video_masked).setVisibility(View.VISIBLE);
int visibilityPercents = VisibilePercentsUtils.getInstance().getVisibilityPercents(childAt);
if (visibilityPercents == 100) {
position = i;
}
}
}
itemPosition = (firstItemPosition + position);
recyclerView.getChildAt(position).findViewById(R.id.video_masked).setVisibility(View.GONE);
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:94) itemPosition --> " + itemPosition + " playerPosition:" + playerPosition);
if (playerPosition == itemPosition) {// 说明还是之前的 item 并没有滑动到下一个
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:109) 还是当前的item 没有变化 继续播放");
} else { // 说明亮起的已经不是当前的item了,是下一个或者之前的那个,我们停止变暗的item的播放
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:120) stopPlayer:" + playerPosition);
VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(playerPosition);
if (childViewHolder != null) {
childViewHolder.stopPlayer();
childViewHolder.unRegisterVideoPlayerListener();// 注意我们需要解除上一个item的监听,不然会注册多个监听
}
playerPosition = itemPosition;
}
}
- 播放完当前的视频,自动滚动到下一个并自动播放。
思路:监听视频播放状态(这里用的是七牛开源的播放器)。
public interface OnVideoPlayerListener {
void onVideoPrepared();
void onVideoCompletion();
void onVideoError(int i,String error);
void onBufferingUpdate();
void onVideoStopped();
void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus);
}
然后再ViewHolder 中实现监听,并在ViewHolder 中写一个接口 ,Activity 中实现这个接口
public interface onHolderVideoPlayerListener {
void videoError();
void videoCompletion();
void videoBuffer();
void videoTips();
void missVideoTips();
}
private onHolderVideoPlayerListener listener;
/**
* 注册监听
*
* @param listener
*/
public void registerVideoPlayerListener(onHolderVideoPlayerListener listener) {
this.listener = listener;
}
/**
* 解除监听
*/
public void unRegisterVideoPlayerListener() {
if (listener != null) {
listener = null;
}
}
需要注意的是我们要为 item 注册 和解除监听,否则item 移除屏幕后,还会继续监听,这样就乱套了。
同理,每个item的播放器也是及时的解除监听状态,否则我们拿到的播放进度会有问题。
ViewHolder 实现 OnVideoPlayerListener,
@Override
public void onVideoPrepared() {
}
@Override
public void onVideoCompletion() {
if (listener != null) {
img.setVisibility(View.VISIBLE);
video_masked.setVisibility(View.VISIBLE);
listener.videoCompletion();
lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
}
}
@Override
public void onVideoError(int i, String error) {
if (listener != null) {
listener.videoError();
}
}
@Override
public void onBufferingUpdate() {
}
@Override
public void onVideoStopped() {
img.setVisibility(View.VISIBLE);
video_masked.setVisibility(View.VISIBLE);
lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
}
@Override
public void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus) {
float percent = (float) ((double) currentPosition / (double) mDuration);
DecimalFormat fnum = new DecimalFormat("##0.0");
float c_percent = 0;
c_percent = Float.parseFloat(fnum.format(percent));
if (0.8 <= c_percent) {
if (listener != null) {
listener.videoTips();
}
} else {
if (listener != null) {
listener.missVideoTips();
}
}
}
Activity 的实现思路,看下面代码
implements VideoHolder.onHolderVideoPlayerListener
@Override
public void videoError() {
}
@Override
public void videoCompletion() { //播放完成 播放下一个
int p = (itemPosition + 1);
rl_video.smoothScrollToPosition(p);
}
@Override
public void videoBuffer() {
}
@Override
public void videoTips() {
mTv.setVisibility(View.VISIBLE);
}
@Override
public void missVideoTips() {
mTv.setVisibility(View.GONE);
}
需要注意的是 当我们调用 rl_video.smoothScrollToPosition(p); recyclerview的滚动监听会回调,我们需要在这样滚动的时候这样处理
同理 ,下面这段代码也处理了,我们用手拖动时,若当前item 百分比最大 视频继续播放。
itemPosition = (firstItemPosition + position);
recyclerView.getChildAt(position).findViewById(R.id.video_masked).setVisibility(View.GONE);
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:94) itemPosition --> " + itemPosition + " playerPosition:" + playerPosition);
if (playerPosition == itemPosition) {// 说明还是之前的 item 并没有滑动到下一个
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:109) 还是当前的item 没有变化 继续播放");
} else { // 说明亮起的已经不是当前的item了,是下一个或者之前的那个,我们停止变暗的item的播放
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:120) stopPlayer:" + playerPosition);
VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(playerPosition);
if (childViewHolder != null) {
childViewHolder.stopPlayer();
childViewHolder.unRegisterVideoPlayerListener();// 注意我们需要解除上一个item的监听,不然会注册多个监听
}
playerPosition = itemPosition;
}
- 正在播放的当前视频,快要播放完毕时,弹出TextView提示播放下一个,点击TextView
思路:这个就很简单了,上面的代码也有给出。我说一下思路,已知我们在ViewHolder 中监听视频播放状态同时也监听了播放进度,如下代码:
@Override
public void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus) {
float percent = (float) ((double) currentPosition / (double) mDuration);
DecimalFormat fnum = new DecimalFormat("##0.0");
float c_percent = 0;
c_percent = Float.parseFloat(fnum.format(percent));
if (0.8 <= c_percent) {
if (listener != null) {
listener.videoTips();
}
} else {
if (listener != null) {
listener.missVideoTips();
}
}
}
已知我们在Activity 中实现了监听 onHolderVideoPlayerListener ,这样就很好去处理了
@Override
public void videoTips() {
mTv.setVisibility(View.VISIBLE);
}
@Override
public void missVideoTips() {
mTv.setVisibility(View.GONE);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv:
int p = (itemPosition + 1);
rl_video.smoothScrollToPosition(p);
break;
}
}
这样我们就实现了所有的功能点,这里视频播放器我用的是七牛开源的播放器,具体的封装就不写出来了,都是写简单的监听以及回调。下面我给出主要的代码,供大家参考。
VideoHolder
package com.linksu.mydemo;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.linksu.video_manager_library.listener.OnVideoPlayerListener;
import com.linksu.video_manager_library.ui.LVideoView;
import java.text.DecimalFormat;
import java.util.List;
/**
* ================================================
* 作 者:linksus
* 版 本:1.0
* 创建日期:7/27 0027
* 描 述:
* 修订历史:
* ================================================
*/
public class VideoHolder extends RecyclerView.ViewHolder implements OnVideoPlayerListener {
View video_masked;
TextView item_tv;
List<String> mlist;
ImageView img;
LVideoView lVideoView;
public VideoHolder(View itemView, List<String> mlist) {
super(itemView);
this.item_tv = (TextView) itemView.findViewById(R.id.item_tv);
this.lVideoView = (LVideoView) itemView.findViewById(R.id.lvideoview);
this.mlist = mlist;
this.img = (ImageView) itemView.findViewById(R.id.img);
this.video_masked = itemView.findViewById(R.id.video_masked);
}
public void update(int position) {
String s = mlist.get(position);
item_tv.setText(s);
}
public void player(int position) {
String url = mlist.get(position);
if (lVideoView != null) {
lVideoView.setOnVideoPlayerListener(this);
lVideoView.startLive(url);
}
}
public void stopPlayer() {
if (lVideoView != null) {
lVideoView.stopVideoPlay();
}
}
public void goneMasked() {
img.setVisibility(View.GONE);
video_masked.setVisibility(View.GONE);
}
@Override
public void onVideoPrepared() {
}
@Override
public void onVideoCompletion() {
if (listener != null) {
img.setVisibility(View.VISIBLE);
video_masked.setVisibility(View.VISIBLE);
listener.videoCompletion();
lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
}
}
@Override
public void onVideoError(int i, String error) {
if (listener != null) {
listener.videoError();
}
}
@Override
public void onBufferingUpdate() {
}
@Override
public void onVideoStopped() {
img.setVisibility(View.VISIBLE);
video_masked.setVisibility(View.VISIBLE);
lVideoView.unOnVideoPlayerListener();// 注意 注销监听 否则之前的item 都会有监听
}
@Override
public void onVideoPlayingPro(long currentPosition, long mDuration, int mPlayStatus) {
float percent = (float) ((double) currentPosition / (double) mDuration);
DecimalFormat fnum = new DecimalFormat("##0.0");
float c_percent = 0;
c_percent = Float.parseFloat(fnum.format(percent));
if (0.8 <= c_percent) {
if (listener != null) {
listener.videoTips();
}
} else {
if (listener != null) {
listener.missVideoTips();
}
}
}
public interface onHolderVideoPlayerListener {
void videoError();
void videoCompletion();
void videoBuffer();
void videoTips();
void missVideoTips();
}
private onHolderVideoPlayerListener listener;
/**
* 注册监听
*
* @param listener
*/
public void registerVideoPlayerListener(onHolderVideoPlayerListener listener) {
this.listener = listener;
}
/**
* 解除监听
*/
public void unRegisterVideoPlayerListener() {
if (listener != null) {
listener = null;
}
}
}
VideoAdapter
package com.linksu.mydemo;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
/**
* ================================================
* 作 者:linksus
* 版 本:1.0
* 创建日期:7/27 0027
* 描 述:
* 修订历史:
* ================================================
*/
public class VideoAdapter extends RecyclerView.Adapter<VideoHolder> {
private List<String> mlist;
private Context context;
public VideoAdapter(List<String> mlist, Context context) {
this.mlist = mlist;
this.context = context;
}
@Override
public VideoHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View inflater = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
return new VideoHolder(inflater, mlist);
}
@Override
public void onBindViewHolder(VideoHolder holder, int position) {
holder.update(position);
}
@Override
public int getItemCount() {
return mlist.size();
}
@Override
public long getItemId(int position) {
return position;
}
}
MainActivity
package com.linksu.mydemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements VideoHolder.onHolderVideoPlayerListener, View.OnClickListener {
private RecyclerView rl_video;
private LinearLayoutManager layoutManager;
private VideoAdapter adapter;
private List<String> mList = new ArrayList<>();
private boolean mScrollState = false;//是否处于滚动状态
private int lastItemPosition;
private int firstItemPosition;
private int visibleItemCount;
private TextView mTv;
private int maxPercents = 0; // 最大显示百分比
private int position = 0; // 最大显示百分比的屏幕内的子view的位置
private int itemPosition = 0;// item 的位置
private int playerPosition = 0;//正在播放item 的位置
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < 30; i++) {
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/149999980163e892f63bf5cb85.mp4");
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/1499914158feea8c512f348b4a.mp4");
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/14991545431a9b3f9b6dd22db2.mp4");
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201707/14991537461a9b3f9b6dd22db2.mp4");
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201706/14963009301c0fd671d0e3ae1b.mp4");
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201705/14962013161a9b3f9b6dd22db2.mp4");
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201705/14958540601a9b3f9b6dd22db2.mp4");
mList.add("http://rmrbtest-image.peopleapp.com/upload/video/201705/14957861291a9b3f9b6dd22db2.mp4");
}
rl_video = (RecyclerView) findViewById(R.id.rl_video);
mTv = (TextView) findViewById(R.id.tv);
mTv.setOnClickListener(this);
layoutManager = new LinearLayoutManager(this);
rl_video.setLayoutManager(layoutManager);
adapter = new VideoAdapter(mList, this);
rl_video.setAdapter(adapter);
rl_video.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE://停止滑动
mScrollState = false;
//滑动停止和松开手指时,调用此方法 进行播放
aoutPlayVideo(recyclerView);
break;
case RecyclerView.SCROLL_STATE_DRAGGING://用户用手指滚动
mScrollState = true;
break;
case RecyclerView.SCROLL_STATE_SETTLING://自动滚动
mScrollState = true;
break;
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearManager = (LinearLayoutManager) layoutManager;
//获取最后一个可见view的位置
lastItemPosition = linearManager.findLastVisibleItemPosition();
//获取第一个可见view的位置
firstItemPosition = linearManager.findFirstVisibleItemPosition();
//获取可见view的总数
visibleItemCount = linearManager.getChildCount();
if (mScrollState) { //滚动
srcollVisible(recyclerView, firstItemPosition, lastItemPosition, visibleItemCount);
} else { //停止 第一次进入时调用此方法,进行播放
aoutPlayVideo(recyclerView);
}
}
}
});
}
/**
* 滚动时 判断哪个view 显示百分比最大,哪个最大 视图亮起
*
* @param recyclerView
* @param firstItemPosition
* @param lastItemPosition
* @param visibleItemCount 屏幕显示的item数量
*/
private void srcollVisible(RecyclerView recyclerView, int firstItemPosition, int lastItemPosition, int visibleItemCount) {
for (int i = 0; i < visibleItemCount; i++) {
if (recyclerView != null) {
View childAt = recyclerView.getChildAt(i).findViewById(R.id.visiabile);
recyclerView.getChildAt(i).findViewById(R.id.video_masked).setVisibility(View.VISIBLE);
int visibilityPercents = VisibilePercentsUtils.getInstance().getVisibilityPercents(childAt);
if (visibilityPercents == 100) {
position = i;
}
}
}
itemPosition = (firstItemPosition + position);
recyclerView.getChildAt(position).findViewById(R.id.video_masked).setVisibility(View.GONE);
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:94) itemPosition --> " + itemPosition + " playerPosition:" + playerPosition);
if (playerPosition == itemPosition) {// 说明还是之前的 item 并没有滑动到下一个
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:109) 还是当前的item 没有变化 继续播放");
} else { // 说明亮起的已经不是当前的item了,是下一个或者之前的那个,我们停止变暗的item的播放
Log.e("linksu MainActivity",
"srcollVisible(MainActivity.java:120) stopPlayer:" + playerPosition);
VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(playerPosition);
if (childViewHolder != null) {
childViewHolder.stopPlayer();
childViewHolder.unRegisterVideoPlayerListener();// 注意我们需要解除上一个item的监听,不然会注册多个监听
}
playerPosition = itemPosition;
}
}
/**
* 1.停止滚动手指抬起时 开始播放视频
*
* @param recyclerView
*/
private void aoutPlayVideo(final RecyclerView recyclerView) {
Log.e("linksu MainActivity",
"aoutPlayVideo(MainActivity.java:112) position --> " + position);
VideoHolder childViewHolder = (VideoHolder) recyclerView.findViewHolderForAdapterPosition(itemPosition);
if (childViewHolder != null) {
childViewHolder.registerVideoPlayerListener(this);
childViewHolder.goneMasked();
childViewHolder.player(itemPosition);
}
}
@Override
public void videoError() {
}
@Override
public void videoCompletion() { //播放完成 播放下一个
int p = (itemPosition + 1);
rl_video.smoothScrollToPosition(p);
}
@Override
public void videoBuffer() {
}
@Override
public void videoTips() {
mTv.setVisibility(View.VISIBLE);
}
@Override
public void missVideoTips() {
mTv.setVisibility(View.GONE);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv:
int p = (itemPosition + 1);
rl_video.smoothScrollToPosition(p);
break;
}
}
}
以上就是主要实现的代码以及思路,纯手工,从思考到实现,不管实现什么功能思路最重要了,只要有了思路就可以用代码写出来,有不理解的地方可以留言给我。
推荐 好用的翻墙工具 ,主要是稳定。
仿网易/QQ空间视频列表滚动连播炫酷效果(V2.0 填坑之路) 想看源码的,看这篇文章。