零、前言
代码语言:javascript复制对于视频的播放,Android有内置的VideoView,用起来非常简单 本篇从自定义VideoView来封装MediaPlayer开始说起
<VideoView
android:id="@ id/id_vv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
---->[使用:PlayerActivity.kt]------------------------------------------------
id_vv.setMediaController(MediaController(this))
id_vv.setVideoPath("/sdcard/toly/sh.mp4")
本文聚焦
代码语言:javascript复制[1].自定义VideoView结合SurfaceView和MediaPlayer来播放视频
[2].使用媒体库的ContentProvider查询手机中视频,并列表显示
[3].更改视频的宽高以及适应横竖屏切换
[4].自定义控制界面以及倍速播放
[5].视频封面图(视频帧)的获取
[6].播放网络视频及seekBar的第二进度和缓存进度监听
一、简易版:MediaPlayer SurfaceView MediaController
代码语言:javascript复制角色:
MediaPlayer 视频处理器
SurfaceView 视频显示界面
MediaController 视频控制器
1.自定义VideoView继承自SurfaceView
代码语言:javascript复制/**
* 作者:张风捷特烈<br/>
* 时间:2019/3/8/008:12:43<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:视频播放:MediaPlayer SurfaceView MediaController
*/
public class VideoView extends SurfaceView implements MediaController.MediaPlayerControl {
private SurfaceHolder mSurfaceHolder;//SurfaceHolder
private MediaPlayer mMediaPlayer;//媒体播放器
private MediaController mMediaController;//媒体控制器
private int mVideoHeight;//视频宽高
private int mVideoWidth;//视频高
private int mSurfaceHeight;//SurfaceView高
private int mSurfaceWidth;//SurfaceView宽
private boolean isPrepared;//是否已准备好
private Uri mUri;//播放的地址
private int mCurrentPos;//当前进度
private int mDuration = -1;//当前播放视频时长
private int mCurrentBufferPer;//当前缓冲进度--网络
public VideoView(Context context) {
this(context, null);
}
public VideoView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceHolder = holder;
openVideo();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mSurfaceHeight = height;
mSurfaceWidth = width;
if (mMediaPlayer != null && isPrepared) {
initPosition();
mMediaPlayer.start();//开始播放
showCtrl();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mSurfaceHolder = null;
hideController();
releasePlayer();
}
});
}
/**
* 显示控制器
*/
private void showCtrl() {
if (mMediaController != null) {
mMediaController.show();
}
}
/**
* 隐藏控制器
*/
private void hideController() {
if (mMediaController != null) {
mMediaController.hide();
}
}
/**
* 初始化最初位置
*/
private void initPosition() {
if (mCurrentPos != 0) {
mMediaPlayer.seekTo(mCurrentPos);
mCurrentPos = 0;
}
}
private void openVideo() {
if (mUri == null || mSurfaceHolder == null) {
return;
}
isPrepared = false;//没有准备完成
releasePlayer();
mMediaPlayer = new MediaPlayer();
try {
mMediaPlayer.setDataSource(getContext(), mUri);
mMediaPlayer.setDisplay(mSurfaceHolder);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setScreenOnWhilePlaying(true);//播放时屏幕一直亮着
mMediaPlayer.prepareAsync();//异步准备
attach2Ctrl();//绑定媒体控制器
} catch (IOException e) {
e.printStackTrace();
}
//准备监听
mMediaPlayer.setOnPreparedListener(mp -> {
isPrepared = true;
if (mMediaController != null) {//控制器可用
mMediaController.setEnabled(true);
}
if (mOnPreparedListener != null) {//补偿回调
mOnPreparedListener.onPrepared(mp);
}
mVideoWidth = mp.getVideoWidth();
mVideoHeight = mp.getVideoHeight();
if (mVideoWidth != 0 && mVideoHeight != 0) {
getHolder().setFixedSize(mVideoWidth, mVideoHeight);
//开始初始化
initPosition();
if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
if (!isPlaying() && mCurrentPos != 0 || getCurrentPosition() > 0) {
if (mMediaController != null) {
mMediaController.show(0);
}
}
}
}
});
//尺寸改变监听
mMediaPlayer.setOnVideoSizeChangedListener((mp, width, height) -> {
mVideoWidth = mp.getVideoWidth();
mVideoHeight = mp.getVideoHeight();
if (mOnSizeChanged != null) {
mOnSizeChanged.onSizeChange();
}
if (mVideoWidth != 0 && mVideoHeight != 0) {
getHolder().setFixedSize(mVideoWidth, mVideoHeight);
}
});
//完成监听
mMediaPlayer.setOnCompletionListener(mp -> {
hideController();
start();
if (mOnCompletionListener != null) {
mOnCompletionListener.onCompletion(mp);
}
});
//错误监听
mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
hideController();
if (mOnErrorListener != null) {
mOnErrorListener.onError(mp, what, extra);
}
return true;
});
mMediaPlayer.setOnBufferingUpdateListener((mp, pre) -> {
mCurrentBufferPer = pre;
});
}
/**
* 释放播放器
*/
private void releasePlayer() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
private void attach2Ctrl() {
if (mMediaPlayer != null && mMediaController != null) {
mMediaController.setMediaPlayer(this);
View anchor = this.getParent() instanceof View ? (View) this.getParent() : this;
mMediaController.setAnchorView(anchor);
mMediaController.setEnabled(true);
}
}
public void setVideoPath(String path) {
mUri = Uri.parse(path);
setVideoURI(mUri);
}
public void setVideoURI(Uri uri) {
mUri = uri;
mCurrentPos = 0;
openVideo();//打开视频
requestLayout();//更新界面
invalidate();
}
public void setMediaController(MediaController mediaController) {
hideController();
mMediaController = mediaController;
attach2Ctrl();
}
public void stopPlay() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
private void toggle() {
if (mMediaController.isShowing()) {
mMediaController.hide();
} else {
mMediaController.show();
}
}
private boolean canPlay() {
return mMediaPlayer != null && isPrepared;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isPrepared && mMediaController != null && mMediaPlayer != null) {
toggle();
}
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = adjustSize(mVideoWidth, widthMeasureSpec);
int h = adjustSize(mVideoHeight, heightMeasureSpec);
setMeasuredDimension(w, h);
}
public int adjustSize(int size, int measureSpec) {
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int len = MeasureSpec.getMode(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(size, len);
break;
case MeasureSpec.EXACTLY:
result = len;
break;
}
return result;
}
//----------------------------------------------------------------
//------------MediaPlayerControl接口函数---------------------------
//----------------------------------------------------------------
@Override
public void start() {
if (canPlay()) {
mMediaPlayer.start();
}
}
@Override
public void pause() {
if (canPlay() && mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
}
}
@Override
public int getDuration() {
if (canPlay()) {
if (mDuration > 0) {
return mDuration;
}
mDuration = mMediaPlayer.getDuration();
return mDuration;
}
mDuration = -1;
return mDuration;
}
@Override
public int getCurrentPosition() {
if (canPlay()) {
return mMediaPlayer.getCurrentPosition();
}
return 0;
}
@Override
public void seekTo(int pos) {
if (canPlay()) {
mMediaPlayer.seekTo(pos);
} else {
mCurrentPos = pos;
}
}
@Override
public boolean isPlaying() {
if (canPlay()) {
return mMediaPlayer.isPlaying();
}
return false;
}
@Override
public int getBufferPercentage() {
if (canPlay()) {
return mCurrentBufferPer;
}
return 0;
}
@Override
public boolean canPause() {
return true;
}
@Override
public boolean canSeekBackward() {
return true;
}
@Override
public boolean canSeekForward() {
return true;
}
@Override
public int getAudioSessionId() {
return 0;
}
//----------------------------------------------------------------
//------------补偿回调---------------------------
//----------------------------------------------------------------
private MediaPlayer.OnPreparedListener mOnPreparedListener;
private MediaPlayer.OnCompletionListener mOnCompletionListener;
private MediaPlayer.OnErrorListener mOnErrorListener;
public void setOnPreparedListener(MediaPlayer.OnPreparedListener onPreparedListener) {
mOnPreparedListener = onPreparedListener;
}
public void setOnCompletionListener(MediaPlayer.OnCompletionListener onCompletionListener) {
mOnCompletionListener = onCompletionListener;
}
public void setOnErrorListener(MediaPlayer.OnErrorListener onErrorListener) {
mOnErrorListener = onErrorListener;
}
public interface OnSizeChanged {
void onSizeChange();
}
private OnSizeChanged mOnSizeChanged;
public void setOnSizeChanged(OnSizeChanged onSizeChanged) {
mOnSizeChanged = onSizeChanged;
}
}
2.根据路径使用测试
代码语言:javascript复制简单一点,可以用系统自带的控制器:MediaController,不过丑到爆炸 文件权限自理:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
---->[activity_main.xml]------------------------------------------------
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.toly1994.ivideo.widget.VideoView
android:id="@ id/id_vv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
---->[使用:PlayerActivity.kt]------------------------------------------------
id_vv.setMediaController(MediaController(this))
id_vv.setVideoPath("/sdcard/toly/sh.mp4")
3.获取所有的视频并根据插入时间降序排列
代码语言:javascript复制/**
* 作者:张风捷特烈<br/>
* 时间:2018/10/30 0030:18:38<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:视频ContentProvide相关操作---生成视频List
*/
public class VideoScanner {
static String[] projection = new String[]{
MediaStore.Video.Media._ID,//ID
MediaStore.Video.Media.TITLE,//名称
MediaStore.Video.Media.DURATION,//时长
MediaStore.Video.Media.DATA,//路径
MediaStore.Video.Media.SIZE,//大小
MediaStore.Video.Media.DATE_ADDED//添加的时间
};
/**
* 歌曲集合
*/
private static List<VideoInfo> videos = new ArrayList<>();
/**
* 读取音频
*/
public static List<VideoInfo> loadVideo(final Context context) {
if (videos.size() != 0) {
return videos;
}
Cursor cursor = context.getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection, "", null,
"date_added desc", null);
// 根据字段获取数据库中数据的索引
int songIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
int titleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
int durationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION);
int dataUrlIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);
int sizeIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE);
int addDateIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED);
while (cursor.moveToNext()) {
long videoId = cursor.getLong(songIdIdx);//获取id
String title = cursor.getString(titleIdx);//获取名字
String dataUrl = cursor.getString(dataUrlIdx);//获取路径
long duration = cursor.getLong(durationIdx);//获取时长
long size = cursor.getLong(sizeIdx);//获取大小
long addDate = cursor.getLong(addDateIdx);//加入时间
videos.add(new VideoInfo(videoId, title, dataUrl, duration, size, addDate));
}
return videos;
}
}
4.RecyclerView装一下Video信息
代码语言:javascript复制关于封面预览图等会在倒腾,布局什么的就不贴了,自己写 当点击的时候,跳转到刚才的那个播放Activity,用Intent传递视频路径
---->[HomeAdapter#onBindViewHolder]-------------------------------------------
holder.mIvCover.setOnClickListener(v -> {
Intent intent = new Intent(mContext, PlayerActivity.class);
intent.putExtra("video-path", videoInfo.getDataUrl());
mContext.startActivity(intent);
});
---->[附赠一个视频时间转化的方法]----------------------------------------
private String format(long duration) {
long time = duration / 1000;
String result = "";
long minus = time / 60;
int hour = 0;
if (minus > 60) {
hour = (int) (minus / 60);
minus = minus % 60;
}
long second = time % 60;
if (hour < 60) {
result = handleNum(hour) ":" handleNum(minus) ":" handleNum(second);
}
return result;
}
private String handleNum(long num) {
return num < 10 ? ("0" num) : (num "");
}
---->[PlayerActivity]-------------------------------------------
val path = intent.getStringExtra("video-path")
id_vv.setMediaController(MediaController(this))
id_vv.setUri(path)
OK 简易版的视频播放器就OK了。
二、界面横竖屏问题
这转个屏,D 都变成 A 了,怎么能忍,赶快修一下
1.关于缩放
代码语言:javascript复制getHolder().setFixedSize(w,h) 测试了一下,然并卵,分辨率没有改变
|-- 来翻一下源码
/**
* Make the surface a fixed size. It will never change from this size.
* When working with a {@link SurfaceView}, this must be called from the
* same thread running the SurfaceView's window.
* 使surface的大小固定。它的大小永远不会改变。
* 当使用SurfaceView时,必须从运行SurfaceView窗口的同一线程调用它。
* @param width The surface's width. surface宽
* @param height The surface's height. surface高
*/
public void setFixedSize(int width, int height);
看来此路不通,那只能求他路
2.直接变更View的尺寸
代码语言:javascript复制public void changeVideoFitSize(int videoW, int videoH, int surfaceW, int surfaceH) {
float videoSizeRate = videoW * 1.0f / videoH;
//横屏下的切换 -- 正常宽高比例
float widthRatePortrait = videoW * 1.0f / surfaceW;
float heightRatePortrait = videoH * 1.0f / surfaceH;
//横屏下的切换 View宽高互换-- 宽高比例
float widthRateLand = videoW * 1.0f / surfaceH;
float heightRateLand = videoH * 1.0f / surfaceW;
float ratio;
if (getResources().getConfiguration().orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {//横屏
//竖屏模式下
ratio = Math.max(widthRatePortrait, heightRatePortrait);
} else {
//横屏模式下
if (videoSizeRate > 1) {
ratio = Math.min(widthRateLand, heightRateLand);
} else {
ratio = Math.max(widthRateLand, heightRateLand);
}
}
//视频宽高分别/最大倍数值 计算出放大后的视频尺寸
videoW = (int) Math.ceil(videoW * 1.0f / ratio);
videoH = (int) Math.ceil(videoH * 1.0f / ratio);
//根据将视频尺寸变更View
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(videoW, videoH);
setLayoutParams(params);
}
|--- 使用:
---->[setOnVideoSizeChangedListener中]---------------------------------------------
changeVideoFitSize(mVideoWidth, mVideoHeight, mSurfaceWidth, mSurfaceHeight);
3.不满屏时居中
代码语言:javascript复制至于怎么居中,我天真的以为在xml里改一下就行了,but,并没用,因为这里是自己玩LayoutParams 所以居中也要用LayoutParams,没办法,走波源码呗。
---->[RelativeLayout#CENTER_IN_PARENT]---------------------
public static final int CENTER_IN_PARENT = 13;
CENTER_IN_PARENT是一个int型控制的,看一下LayoutParams的源码,暴露的方法就那几个,
addRule恰只有一个int入参,应该就是它了
---->[RelativeLayout.LayoutParams#addRule(int)]---------------------
public void addRule(int verb) {
addRule(verb, TRUE);
}
---->[.VideoView#changeVideoFitSize(int, int, int, int)]-------------
---- 轻轻写语句,即可
params.addRule(13);
3.自定义宽高缩放比例
代码语言:javascript复制public void changeVideoSize(float rateX, float rateY) {
changeVideoFitSize(mVideoWidth, mVideoHeight, mSurfaceWidth, mSurfaceHeight, rateX, rateY);
}
public void changeVideoFitSize(
int videoW, int videoH, int surfaceW, int surfaceH,
float rateX, float rateY) {
...
//视频宽高分别/最大倍数值 计算出放大后的视频尺寸
videoW = (int) Math.ceil(videoW * 1.0f / ratio * rateX);
videoH = (int) Math.ceil(videoH * 1.0f / ratio * rateY);
//无法直接设置视频尺寸,将计算出的视频尺寸设置到surfaceView 让视频自动填充。
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(videoW, videoH);
params.addRule(13);
setLayoutParams(params);
}
三、定制操作界面
1.界面操作
代码语言:javascript复制自定义的界面就是根据VideoView中的Api自己实现控制逻辑,细心一点还是不难的,就是麻烦 界面如下,不贴布局了,比较简单,也挺多的,这里说一下显示面板后5秒后隐藏的逻辑
private val mHandler = Handler(Looper.getMainLooper())
root.setOnClickListener {//点击显示面板
showPanel(mHandler)
}
private fun hidePanel() {
id_ll_top.visibility = View.GONE
id_ll_bottom.visibility = View.GONE
id_iv_lock.visibility = View.GONE
}
private fun showPanel(handler: Handler) {
id_ll_top.visibility = View.VISIBLE
id_ll_bottom.visibility = View.VISIBLE
id_iv_lock.visibility = View.VISIBLE
handler.postDelayed(::hidePanel, 5000)
}
2.倍速播放
代码语言:javascript复制二倍速听mv挺搞笑的,API 23 也就是一句Api的事,很方便
/**
* 变速
* @param speed
*/
public void changeSpeed(float speed) {
//API 23 支持
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(speed));
} else {
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(speed));
mMediaPlayer.pause();
}
}
}
|-- 使用数组来控制-----------------------
private var speeds = floatArrayOf(0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2.0f)
private var curSpeedIdx = 2
id_tv_speed.setOnClickListener {
curSpeedIdx
if (curSpeedIdx == speeds.size) {
curSpeedIdx = 0
}
val speed = speeds[curSpeedIdx]
id_vv.changeSpeed(speed)
id_tv_speed.text = "$speed X"
}
3.封面图的获取
获取帧.png 基本上也就这么多了,最后讲一下视频封面帧图片的获取:数了一下这帧大概在15秒 测试了一下秒数越大,获取图片的速度越慢,也就是越卡,所以还是给0吧 如果在Adapter里实时加载会很卡,最好查询的时候就把bitmap放到实体类里,由于封面图不要很大 别把原图给放进去了,小心直接OOM。Bitmap的操作本文就不赘述了。
图片尺寸.png
代码语言:javascript复制---->[HomeAdapter]------------------------
private final MediaMetadataRetriever retriever;
retriever = new MediaMetadataRetriever();
/**
* 获取视频某一帧
*
* @param path 路径
* @param timeMs 毫秒
*/
public Bitmap decodeFrame(String path,long timeMs) {
retriever.setDataSource(path);
Bitmap bitmap = retriever.getFrameAtTime(timeMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST);
if (bitmap == null) {
return null;
}
return bitmap;
}
代码语言:javascript复制此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于给定时间附近或给定时间的数据源相关联的帧(不一定是关键帧)。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a frame (not necessarily a key frame) associated with a data source that
* is located closest to or at the given time.
public static final int OPTION_CLOSEST = 0x03;
此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于(时间上)最接近或给定时间的数据源相关联的同步(或键)帧。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a sync (or key) frame associated with a data source that is located
* closest to (in time) or at the given time.
public static final int OPTION_CLOSEST_SYNC = 0x02;
此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于给定时间之后或指定时间的数据源关联的同步(或键)帧。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a sync (or key) frame associated with a data source that is located
* right after or at the given time.
public static final int OPTION_NEXT_SYNC = 0x01;
此选项与{@link #getFrameAtTime(long,int)}一起使用,以检索与位于给定时间之前或指定时间的数据源关联的同步(或键)帧。
* This option is used with {@link #getFrameAtTime(long, int)} to retrieve
* a sync (or key) frame associated with a data source that is located
* right before or at the given time.
public static final int OPTION_PREVIOUS_SYNC = 0x00;
四、网络视频的播放
1.网络视频
代码语言:javascript复制放在服务器上了,地址:
http://www.toly1994.com:8089/imgs/sh.mp4
,就一句话
id_vv.setVideoPath("http://www.toly1994.com:8089/imgs/sh.mp4")
2.SeekBar的第二进度
代码语言:javascript复制---->[drawable/seekbar_bg.xml]--------------------------------------------
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<solid android:color="#eee" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<solid android:color="#2db334"/>
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<solid android:color="@color/colorAccent"/>
</shape>
</clip>
</item>
</layer-list>
---->[layout/in_player_panel_bottom.xml]---------------------------
<SeekBar
...
android:progressDrawable="@drawable/seekbar_bg"
3.缓存监听
代码语言:javascript复制---->[com.toly1994.ivideo.widget.VideoView]------------------
mMediaPlayer.setOnBufferingUpdateListener((mp, pre) -> {
mCurrentBufferPer = pre;
if (mOnBufferingUpdateListener != null) {
mOnBufferingUpdateListener.update(pre);
}
});
public interface OnBufferingUpdateListener {
void update(int pre);
}
private OnBufferingUpdateListener mOnBufferingUpdateListener;
public void setOnBufferingUpdateListener(OnBufferingUpdateListener onBufferingUpdateListener) {
mOnBufferingUpdateListener = onBufferingUpdateListener;
}
使用:
id_vv.setOnBufferingUpdateListener {
id_sb_progress.secondaryProgress = it
}
Ok 这样就完成了。本篇就这样,更多的功能可以自己去拓展, 搭个后台,弄个简单的网络播放器也未尝不可。
后记:捷文规范
1.本文成长记录及勘误表
项目源码 | 日期 | 备注 |
---|---|---|
无 | 2018-3-9 | Android多媒体之视频播放器(基于MediaPlayer) |
2.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的掘金 | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明 2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正 4----看到这里,我在此感谢你的喜欢与支持