gihub地址:
https://github.com/Doikki/DKVideoPlayer
GitHub - Doikki/DKVideoPlayer: Android Video Player. 安卓视频播放器,封装MediaPlayer、ExoPlayer、IjkPlayer。模仿抖音并实现预加载,列表播放,悬浮播放,广告播放,弹幕,视频水印,视频滤镜
列表播放如图所示:
一、依赖
//添加RecyclerView的依赖包
implementation 'androidx.recyclerview:recyclerview:1.2.1'
// 异步加载图片依赖
implementation 'com.squareup.picasso:picasso:2.5.2'
// 上拉刷新、下来加载依赖
implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.3'
// -- Android:视频播放器dkplayer
//# 必选,内部默认使用系统mediaplayer进行解码
implementation 'com.github.dueeeke.dkplayer:dkplayer-java:3.2.6'
//# 可选,包含StandardVideoController的实现
implementation 'com.github.dueeeke.dkplayer:dkplayer-ui:3.2.6'
//# 可选,使用exoplayer进行解码
implementation 'com.github.dueeeke.dkplayer:player-exo:3.2.6'
//# 可选,使用ijkplayer进行解码
implementation 'com.github.dueeeke.dkplayer:player-ijk:3.2.6'
//# 可选,如需要缓存或者抖音预加载功能请引入此库
implementation 'com.github.dueeeke.dkplayer:videocache:3.2.6'
二、工具类
Tag.java
package com.chy.permission;
/**
* 播放器标签
*/
public final class Tag {
//列表播放
public static final String LIST = "list";
//无缝播放
public static final String SEAMLESS = "seamless";
//画中画
public static final String PIP = "pip";
}
Utils.java
package com.chy.permission;
import android.view.View;
import android.view.ViewParent;
import android.widget.FrameLayout;
import com.dueeeke.videoplayer.player.VideoView;
import com.dueeeke.videoplayer.player.VideoViewConfig;
import com.dueeeke.videoplayer.player.VideoViewManager;
import java.lang.reflect.Field;
public final class Utils {
private Utils() {
}
/**
* 获取当前的播放核心
*/
public static Object getCurrentPlayerFactory() {
VideoViewConfig config = VideoViewManager.getConfig();
Object playerFactory = null;
try {
Field mPlayerFactoryField = config.getClass().getDeclaredField("mPlayerFactory");
mPlayerFactoryField.setAccessible(true);
playerFactory = mPlayerFactoryField.get(config);
} catch (Exception e) {
e.printStackTrace();
}
return playerFactory;
}
/**
* 将View从父控件中移除
*/
public static void removeViewFormParent(View v) {
if (v == null) return;
ViewParent parent = v.getParent();
if (parent instanceof FrameLayout) {
((FrameLayout) parent).removeView(v);
}
}
/**
* Returns a string containing player state debugging information.
*/
public static String playState2str(int state) {
String playStateString;
switch (state) {
default:
case VideoView.STATE_IDLE:
playStateString = "idle";
break;
case VideoView.STATE_PREPARING:
playStateString = "preparing";
break;
case VideoView.STATE_PREPARED:
playStateString = "prepared";
break;
case VideoView.STATE_PLAYING:
playStateString = "playing";
break;
case VideoView.STATE_PAUSED:
playStateString = "pause";
break;
case VideoView.STATE_BUFFERING:
playStateString = "buffering";
break;
case VideoView.STATE_BUFFERED:
playStateString = "buffered";
break;
case VideoView.STATE_PLAYBACK_COMPLETED:
playStateString = "playback completed";
break;
case VideoView.STATE_ERROR:
playStateString = "error";
break;
}
return String.format("playState: %s", playStateString);
}
/**
* Returns a string containing player state debugging information.
*/
public static String playerState2str(int state) {
String playerStateString;
switch (state) {
default:
case VideoView.PLAYER_NORMAL:
playerStateString = "normal";
break;
case VideoView.PLAYER_FULL_SCREEN:
playerStateString = "full screen";
break;
case VideoView.PLAYER_TINY_SCREEN:
playerStateString = "tiny screen";
break;
}
return String.format("playerState: %s", playerStateString);
}
}
VideoAdapter.java
package com.chy.demoprj.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.chy.demoprj.R;
import com.dueeeke.videocontroller.component.PrepareView;
import com.squareup.picasso.Picasso;
import java.util.HashMap;
import java.util.List;
public class VideoAdapter extends RecyclerView.Adapter {
private Context context;
private List> datas;
private OnItemChildClickListener mOnItemChildClickListener;// 播放控件点击事件
/**
* 构造函数
* */
public VideoAdapter(Context context,List> datas){
this.context = context;
this.datas = datas;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_video_layout,parent,false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,int position) {
ViewHolder vh = (ViewHolder) holder;
HashMap entity = datas.get(position);
// 设置播放占位图(不设置默认黑色)
Picasso.with(context)
.load(entity.get("thumbUrl"))
.into(vh.mThumb);
vh.mPosition = position;
}
@Override
public int getItemCount() {
return datas.size();
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
public FrameLayout mPlayerContainer;// 容器
public PrepareView mPrepareView;// 播放视图
public ImageView mThumb;// 缩略图
public int mPosition;// 当前视图index
public ViewHolder(@NonNull View itemView) {
super(itemView);
mPlayerContainer = itemView.findViewById(R.id.player_container);
mPrepareView = itemView.findViewById(R.id.prepare_view);
mThumb = mPrepareView.findViewById(R.id.thumb);
/**
* 添加点击事件
* */
if (mOnItemChildClickListener != null) {
mPlayerContainer.setOnClickListener(this);
}
//通过tag将ViewHolder和itemView绑定
itemView.setTag(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.player_container){
if (mOnItemChildClickListener != null){
mOnItemChildClickListener.onItemChildClick(mPosition);
}
}
}
}
/**
* 点击事件接口
* */
public interface OnItemChildClickListener{
void onItemChildClick(int position);
}
/**
* 点击事件回调函数
* */
public void setOnItemChildClickListener(OnItemChildClickListener onItemChildClickListener) {
mOnItemChildClickListener = onItemChildClickListener;
}
}
三、布局
activity_main.xml
item_video_layout.xml
四、实现
package com.chy.demoprj;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.Manifest;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.Toast;
import com.chy.demoprj.adapter.VideoAdapter;
import com.chy.permission.PermissionUtils;
import com.chy.permission.Tag;
import com.chy.permission.Utils;
import com.dueeeke.videocontroller.StandardVideoController;
import com.dueeeke.videocontroller.component.CompleteView;
import com.dueeeke.videocontroller.component.ErrorView;
import com.dueeeke.videocontroller.component.GestureView;
import com.dueeeke.videocontroller.component.TitleView;
import com.dueeeke.videocontroller.component.VodControlView;
import com.dueeeke.videoplayer.player.VideoView;
import com.dueeeke.videoplayer.player.VideoViewManager;
import com.scwang.smartrefresh.layout.api.RefreshLayout;
import com.scwang.smartrefresh.layout.listener.OnLoadMoreListener;
import com.scwang.smartrefresh.layout.listener.OnRefreshListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_PERMISSION_CODE = 0;// 权限所用
// 动态申请权限
private String[] permissions = {
Manifest.permission.INTERNET,// 网络权限
Manifest.permission.WRITE_EXTERNAL_STORAGE,// 写入数据权限
Manifest.permission.READ_EXTERNAL_STORAGE,// 读取数据权限
Manifest.permission.ACCESS_FINE_LOCATION,// 定位权限
Manifest.permission.ACCESS_COARSE_LOCATION // 获取基站的服务信号权限,以便获取位置信息
};
private RefreshLayout refreshLayout;
private RecyclerView recyclerView;// 列表
private LinearLayoutManager layoutManager;
private List> datas = new ArrayList<>();
// 视频播放-控件
protected VideoView mVideoView;
protected StandardVideoController mController;
protected ErrorView mErrorView;
protected CompleteView mCompleteView;
protected TitleView mTitleView;
/**
* 当前播放的位置
* */
protected int mCurPos = -1;
/**
* 上次播放的位置,用于页面切换回来继续播放
* */
protected int mLastPos = mCurPos;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getPermission();
initData();
initControls();
initVideoView();
}
/**
* 权限
* */
private void getPermission(){
boolean flage = PermissionUtils.hasPermissions(MainActivity.this,permissions);
if (flage) {
System.out.println("权限获取成功!");
} else {
PermissionUtils.requestPermissions(MainActivity.this, REQUEST_PERMISSION_CODE, permissions);
}
}
/**
* 数据初始化
* */
private void initData(){
HashMap hashMap = null;
// 占位图
String thumbUrl = "https://p1-xg.byteimg.com/img/tos-cn-p-0000/527b08d0f31d4705a4d8f4a72120948c~tplv-crop-center:1041:582.jpg";
// 视频播放地址
String playerUrl = "https://vd4.bdstatic.com/mda-pdhb52ikamv3bdb7/sc/cae_h264/1681819063478400576/mda-pdhb52ikamv3bdb7.mp4";
// 循环添加数据
for (int i=0,len=8;i<=len;i++){
hashMap = new HashMap<>();
hashMap.put("thumbUrl",thumbUrl);
hashMap.put("playerUrl",playerUrl);
hashMap.put("vtitle","播放标题"+i);
datas.add(hashMap);
}
}
/**
* 初始化控件
* */
private void initControls(){
refreshLayout = findViewById(R.id.refreshLayout);
// 下拉刷新事件
refreshLayout.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh(@NonNull RefreshLayout refreshLayout) {
refreshLayout.finishRefresh(2000/*,false*/);// 传入false表示刷新失败
}
});
// 上拉加载事件
refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadMore(@NonNull RefreshLayout refreshLayout) {
refreshLayout.finishLoadMore(2000/*,false*/);// 传入false表示加载失败
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplication(),"没有更多数据",Toast.LENGTH_SHORT).show();
}
});
}
});
recyclerView = findViewById(R.id.recyclerView);
// 设置布局
layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
// 配置器
VideoAdapter videoAdapter = new VideoAdapter(this,datas);
// 设置点击事件
videoAdapter.setOnItemChildClickListener(new VideoAdapter.OnItemChildClickListener() {
@Override
public void onItemChildClick(int position) {
/**
* PrepareView被点击
*/
startPlay(position);
}
});
recyclerView.setAdapter(videoAdapter);
// RecyclerView监听事件
recyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
FrameLayout playerContainer = view.findViewById(R.id.player_container);
View v = playerContainer.getChildAt(0);
if (v != null && v == mVideoView && !mVideoView.isFullScreen()){
releaseVideoView();
}
}
});
}
/**
* 播放控件初始化
* */
protected void initVideoView() {
mVideoView = new VideoView(this);
mVideoView.setOnStateChangeListener(new VideoView.SimpleOnStateChangeListener() {
@Override
public void onPlayStateChanged(int playState) {
//监听VideoViewManager释放,重置状态
if (playState == com.dueeeke.videoplayer.player.VideoView.STATE_IDLE) {
Utils.removeViewFormParent(mVideoView);
mLastPos = mCurPos;
mCurPos = -1;
}
}
});
mController = new StandardVideoController(this);
mErrorView = new ErrorView(this);
mController.addControlComponent(mErrorView);
mCompleteView = new CompleteView(this);
mController.addControlComponent(mCompleteView);
mTitleView = new TitleView(this);
mController.addControlComponent(mTitleView);
mController.addControlComponent(new VodControlView(this));
mController.addControlComponent(new GestureView(this));
mController.setEnableOrientation(true);
mVideoView.setVideoController(mController);
}
@Override
public void onPause() {
super.onPause();
pause();
}
/**
* 由于onPause必须调用super。故增加此方法,
* 子类将会重写此方法,改变onPause的逻辑
*/
protected void pause() {
releaseVideoView();
}
@Override
public void onResume() {
super.onResume();
resume();
}
/**
* 由于onResume必须调用super。故增加此方法,
* 子类将会重写此方法,改变onResume的逻辑
*/
protected void resume() {
if (mLastPos == -1)
return;
//恢复上次播放的位置
startPlay(mLastPos);
}
/**
* 开始播放
*
* @param position 列表位置
*/
protected void startPlay(int position) {
if (mCurPos == position) return;
if (mCurPos != -1) {
releaseVideoView();
}
HashMap entity = datas.get(position);
//边播边存
// String proxyUrl = ProxyVideoCacheManager.getProxy(getActivity()).getProxyUrl(videoBean.getUrl());
// mVideoView.setUrl(proxyUrl);
String playurl = entity.get("playerUrl");
mVideoView.setUrl(playurl);
mTitleView.setTitle(entity.get("vtitle"));
View itemView = layoutManager.findViewByPosition(position);
if (itemView == null) return;
VideoAdapter.ViewHolder viewHolder = (VideoAdapter.ViewHolder) itemView.getTag();
//把列表中预置的PrepareView添加到控制器中,注意isPrivate此处只能为true。
mController.addControlComponent(viewHolder.mPrepareView, true);
Utils.removeViewFormParent(mVideoView);
viewHolder.mPlayerContainer.addView(mVideoView, 0);
//播放之前将VideoView添加到VideoViewManager以便在别的页面也能操作它
getVideoViewManager().add(mVideoView, Tag.LIST);
mVideoView.start();
mCurPos = position;
}
/**
* 释放播放控件
* */
private void releaseVideoView() {
mVideoView.release();
if (mVideoView.isFullScreen()) {
mVideoView.stopFullScreen();
}
if (this.getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
mCurPos = -1;
}
/**
* 创建播放管理类
* */
protected VideoViewManager getVideoViewManager(){
return VideoViewManager.instance();
}
}