一、前言
前一段时间在做视频开发,由于本人刚接触视频开发这块,所以
领导没有对我提很高的要求,仅仅要求能够播放本地视频即可。
我想怎么简单怎么做。于是选择用Android VideoView控件来播放视频
(后面发现VideoView的灵活性实在太差,我不想吐槽)。
最终的效果图:
视频全屏效果
这次的任务主要难度在于进度条这个控件。各位可以从上面的两张图中看到,进度条被分
为三段。每段表示一个视频,并且每个视频的长度不一,也就意味着每段视频进度条的前进速度是不相同的。
难点总结:
1、自定义控件进度条的绘制
2、如何计算视频的当前进度
3、如何控制进度条(线程)的暂停,开启。
知识点总结:
1、Android View的绘制流程。
2、Java线程。
3、Android Canvas类的基本用法。
4、VideoView和ProgressBar的基本用法。
5、Activity生命周期。
二、自定义CustomProgressBar控件
1、创建一个CustomProgress类继承ProgressBar
//绘制进度条的X,Y坐标 private int marginXY = 0; //进度条背景,进度条,分隔线的paint private Paint progressbarBackgroundPaint, progressbarPaint, separtatedPaint; //视频总数 private int videoSum = 6; //画笔的宽度 private int strokeWidth = 20; //进度条线程实例 private CustomProgressRunnable customProgressRunnable; //进度条绘制标记位 private boolean startDrawProgress = false; //进度条的背景颜色 private int background; //分隔线的颜色 private int separtatedLineBackground; //进度条的颜色 private int progressbarBackground; //分隔线的宽度 private int separtatedLineWidth; //线程池 private ExecutorService executorService;
这里有个变量需要特别注意的就是strokeWidth。这是一个指定画笔宽度的变量。后面绘制的进度条实质上是
一条线,但是一条线太细了,所以我把这条线的宽度设宽一点,默认设置为20。这样这条线看起来就像进度条了。
2、构造方法初始化
public CustomProgress(Context context , AttributeSet attrs, int defStyleAttr) { super (context, attrs , defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomProgress) ; videoSum = typedArray.getInt(R.styleable.CustomProgress_CP_Video_Sum, 3); background = typedArray.getColor(R.styleable.CustomProgress_CP_Background, Color.parseColor("#F1F1F1" ));//F1F1F1 separtatedLineBackground = typedArray.getColor(R.styleable.CustomProgress_CP_Separated_Line_Background, Color.parseColor("#D6D6D6" )); progressbarBackground = typedArray.getColor(R.styleable.CustomProgress_CP_Progressbar_Background, Color.parseColor("#5BD290" ));//5BD290 separtatedLineWidth = typedArray.getDimensionPixelSize(R.styleable.CustomProgress_CP_Separated_Line_Width, 5); typedArray.recycle() ; // progressbarBackgroundPaint = new Paint() ; progressbarBackgroundPaint .setAntiAlias(true) ; progressbarBackgroundPaint .setStrokeCap(Paint.Cap.ROUND) ; progressbarBackgroundPaint .setStrokeWidth(strokeWidth) ; progressbarBackgroundPaint .setColor(background) ; // progressbarPaint = new Paint() ; progressbarPaint .setAntiAlias(true) ; progressbarPaint .setStrokeWidth(strokeWidth) ; progressbarPaint .setStrokeCap(Paint.Cap.ROUND) ; progressbarPaint .setColor(progressbarBackground) ; separtatedPaint = new Paint() ; separtatedPaint .setAntiAlias(true) ; separtatedPaint .setStrokeWidth(strokeWidth) ; separtatedPaint .setColor(separtatedLineBackground) ; executorService = Executors.newCachedThreadPool(); }
在这里初始化了线程池以及三个笔刷的初始化。
setAntiAlias(true)设置抗锯齿
setStrokeWidth(strokeWidth)设置笔刷的宽度
setStrokeCap(Paint.Cap.ROUND)设置笔刷末端为半圆形
3、重写onMeasure
@Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int size = MeasureSpec.getSize(widthMeasureSpec); int finalWidth = size + getPaddingLeft() + getPaddingRight(); int finalHeight = strokeWidth + getPaddingBottom() + getPaddingTop(); setMeasuredDimension(finalWidth , finalHeight); }
在初始化完成之后,系统就会调用onMeasure方法。开始测量这个类(View)的大小。
如果在xml文件设置layout_width为match_parent或者wrap_content。int size=MeasureSpec.getSize(widthMeasureSpec)得到的值就是填充父布局的最大值。
如果输入的是一个准确数值,int size=准确数值。再将size的值用来作为该视图的宽度。
由进度条的宽度(strokeWidth)作为该视图的高度。layout_height属性在XML文件,无论你设置什么都是无效的!
调用setMeasureDimension(finalWidth,finalHeight)方法设置视图的最终大小。
4、重写onDraw方法
说说我个人的绘制思路:
- 先绘制一个横线作为进度条的背景。
- 在进度条的背景位置上,开始绘制进度条(onDraw方法会被不停的调用,进度条看起来就像在移动)。
- 最后是绘制每段视频之间的分隔线(也许你会问为什么不先绘制分隔线,再绘制进度条。这是因为如果先绘制分隔线,那么后面绘制的进度条就会把分隔线覆盖掉,所以我把绘制的顺序调换了一下)。
绘制细节说明:
- 先看看绘制进度条背景的代码
//开始绘制进度条的X,Y坐标 marginXY = strokeWidth / 2; //绘制背景进度 canvas.drawLine(marginXY, marginXY, getWidth() - marginXY, marginXY, progressbarBackgroundPaint);
也许很多人不理解marginXY是什么意思呢?为了方便解释,请大家再看看下面的图。
strokeWidth的默认值为20,因此它的一半就是marginXY(即值为10)。在这里我们知道,marginXY表示的是蓝色线的横
纵坐标。因此红色那个点的坐标就是(marginXY,marginXY)。所以也可以计算出黄色点的坐标就是(getWidth()-marginXY,marginXY).
从这个图我们也知道,在Android中canvas.drawLine方法绘制直线的时候。它绘制的起始点的纵坐标是从直线的
中间距离开始的,也就是图中红色点的位置。有人可以会问,为什么不是从黑色点开始呢?大家注意一下这条直线
的首位两端。这是一个半圆形状的末端。这是我通过progressbarBackgroundPaint .setStrokeCap(Paint.Cap.ROUND) ;
(你也可以设置为矩形末端,把参数改为Paint.Cap.SQUARE即可)方法设置的。
根据我个人的推论应该是这样理解:这一个整个进度条是由N个圆形紧密相连组成了一条直线。这N个圆形都有一个圆心。
很明显上面图中的红色点和黄色点就是第一个圆和最后一个圆的圆心。(如果有错请勿喷啊。我没看过源码,不知道底层的实现。纯属个人推测)
那么接下来就好办了,为了能够在绘制的过程中显示出首位两端的半圆形状。我们在绘制进度条的时候。应该将绘制的起
始点往后挪一点,就是红色点的坐标(marginXY,marginXY)开始绘制。终点往前挪一点就是黄色点的坐标(getWidth()-marginXY,marginXY)。
调用canvas.drawLine(marginXY, marginXY,
getWidth() - marginXY, marginXY, progressbarBackgroundPaint);将进度条绘制出来。
- 接着看看绘制进度条的代码
//开始绘制进度条 if (startDrawProgress) { //绘制当前进度 canvas.drawLine(marginXY, marginXY, getProgress(), marginXY, progressbarPaint); }
startDrawProgress是一个布尔类型的标记位默认为false。目的是为了避免控件在初始化的时候系统调用onDraw方法
将进度条绘制出来(这个时候我的视频都没点击播放呢,把进度条画出来干啥?)。
在点击视频的时候,通过向外提供的方法。修改startDrawProgress的值为true。
就可以执行canvas.drawLine(marginXY, marginXY,
getProgress(), marginXY, progressbarPaint);了
这些参数我已经在上面讲的很清楚了,在这里不再叙述。
- 最后就是分隔线的绘制
//分隔线的X坐标(注意,这一步必须放在“开始绘制进度条”之后,否则绘制的进度条会把分隔线覆盖。) int separtatedLineX = 0; for (int i = 0; i < videoSum - 1; i++) { //计算分隔线的X坐标 separtatedLineX += (getWidth() - 2 * marginXY) / videoSum; //绘制分隔线 // canvas.drawLine(marginXY + separtatedLineX - (separtatedLineWidth / 2), marginXY, marginXY + separtatedLineX + (separtatedLineWidth/2), marginXY, separtatedPaint); // canvas.drawLine(separtatedLineX , marginXY, separtatedLineX + separtatedLineWidth , marginXY, separtatedPaint); canvas.drawLine(marginXY + separtatedLineX - (separtatedLineWidth / 2), marginXY, marginXY + separtatedLineX + (separtatedLineWidth / 2), marginXY, separtatedPaint); } }
就是separtatedLineX = ( (getWidth()
- 2 * marginXY) / videoSum ) + separtatedLineX ;
getWidth()表示视图的宽度,减去上面所说的红点和黄点两端的横坐标,除以videoSum(这个表示视频的
总数),再加上原来separtatedLineX值,就是视频分隔线横坐标。
每次循环之后得到了separtatedLineX就能把分隔线绘制出来了吗?我尝试调用
canvas.drawLine(separtatedLineX, marginXY, separtatedLineX
+ separtatedLineWidth, marginXY, separtatedPaint);
(这里的separtatedLineWidth表示分隔线的宽度)为了看得更清楚,我特意将分隔线的宽度设宽了。:
很明显的看到,整个分隔线的总体往左偏了。突然想起来,我们刚才在计算separtatedLineX的时候为了得到每个视频的横坐标,把marginXY值给减去了。
那么现在将marginXY的值加上,改成如下形式
canvas.drawLine(separtatedLineX + marginXY, marginXY,
separtatedLineX + separtatedLineWidth + marginXY, marginXY,
separtatedPaint);
再看看运行情况
虽然分隔线偏差没有上面那么离谱但还是没能完全对齐。是什么原因导致没有对齐呢?请看下图
绿色是marginXY的距离,黑色是separtatedLineX的距离。因此
separtatedLineX + marginXY就是红点的横坐标。
separtatedLineX + marginXY+ separtatedLineWidth就是蓝点的横坐标。这中间的宽度就是separtatedLineWidth。
可以很明显的看出。上面的红点应该要在分隔线的中间位置。如下图
因此我要让分隔线对齐就应该让分隔线往左偏移分隔线宽度的二分之一。也就是separtatedLineWidth/2。所以就有
canvas.drawLine(marginXY+ separtatedLineX - (separtatedLineWidth / 2), marginXY, marginXY+
separtatedLineX + (separtatedLineWidth/2), marginXY, separtatedPaint);
运行一下程序
分隔线完美对齐!至此整个自定义控件所需要重写的方法已经写完了。
现在还要解决如何计算进度条的进度的问题。详情请看下面!
三、CustomProgressRunnable内部类
CustomProgress类里面的创建一个CustomProgressRunnable内部类并且让这个内部内继承Runnable接口。
1、CustomProgressRunnable内部类的定义如下变量。
private Handler handler; private Thread thread; private VideoView videoView; //视频进度条的长度 private int perVideoLength; //当前正在播放视频的下标(下标从0开始) private int currentVideoIndex; //线程正在运行标记位 private boolean running = false; //线程正在等待标记位 private boolean waiting = false;
其中比较重要的两个变量就是running和waiting,前者是用来标记线程是否正在运行,后者用来标记线程是否正在等待(暂停)。
2、内部类的构造方法
public CustomProgressRunnable(Handler handler, VideoView videoView, int currentVideoIndex) { this.handler = handler; this.currentVideoIndex = currentVideoIndex; this.perVideoLength = (getWidth() - 2 * marginXY) / videoSum; this.videoView = videoView; thread = new Thread(this); //设置进度条的最大进度值 setMax((currentVideoIndex + 1) * perVideoLength + marginXY); }
下图中绿色线
就是perVideoLength变量的值。
setMax();这是ProgressBar的内部API。用来设置进度条的最大值。
现在假设我要播放第一个视频,那么currentVideoIndex的值就是0(记住currentVideoIndex下标从0开始)。
所以setMax((currentVideoIndex + 1) * perVideoLength + marginXY);如下图
就是第一个视频所能达到的最大进度值。第二三个视频,同理。
其它的都是一些简单的初始化操作,我就不讲了。
3、接下来就要回答两个问题
- 如何计算视频的进度?
- 如何控制进度条的移动?
先计算当前视频的开始进度的位置。接着根据当前视频进度是否小于进度条(setMax)的最大值。如果不小于继续进行循环。
在循环期间,如果用户进行了相应的操作。例如非运行状态则跳出循环。等待状态,则暂停循环。
@Override public void run() { //当前视频开始的位置 int currentVideoStartPosition = (currentVideoIndex * perVideoLength); while (currentVideoStartPosition < getMax()) { synchronized (this) { if (!running) { break; } if (waiting) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } execute(currentVideoStartPosition); } }
最后执行execute方法,计算当前进度条位置的逻辑操作。
public void execute(int currentVideoStartPosition) { if (running && !waiting) { int videoDuration = videoView.getDuration(); if (videoDuration != -1) { //获取比例 float proportion = (float) videoView.getCurrentPosition() / (float) videoDuration; //计算移动进度 int currentVideoPosition = (int) (proportion * perVideoLength); //计算当前最新进度 int newestVideoPosition = currentVideoPosition + currentVideoStartPosition + marginXY; // CommonTool.showLog("测试:" + newestVideoPosition + " " + videoView.getCurrentPosition() + " " + videoView.getDuration() + " " + proportion + " " + perVideoLength); handler.sendMessage(Message.obtain(handler, newestVideoPosition)); } } }
在execute方法中,先判断一下此时线程的状态,如果是线程“正在运行”且“非等待”状态,就往下执行操作。
- 先获取当前视频的总长度videoDuration = videoView.getDuration()。假设第一个视频总长度为1000
- 计算当前视频进度比例float proportion = (float) videoView.getCurrentPosition() / (float) videoDuration;获取到的videoView.getCurrentPosition()假设值为500。得到的视频进度比例为50%
- 将50%乘以下面绿色线的长度。得到当前视频的坐标currentVideoPosition。
4.将视频的起始坐标+当前坐标+开始绘制进度条的坐标相加,就是当前视频的最新坐标。int
newestVideoPosition =currentVideoPosition + currentVideoStartPosition + marginXY;此时currentVideoStartPosition的值为0,红色线的marginXY的值, 绿色线是currentVideoPosition的值。
6、最后通过handler.sendMessage(Message.obtain(handler, newestVideoPosition));将当前视频的最新坐标发送给主线程(即UI线程)。
这里的TestHandler类就是用来接收第6步骤的newestVideoPosition的值。temp变量得到发送过来的坐标值。
再调用setProgress(temp)方法,设置当前进度条的进度。setProgress方法的底层代码会重新绘制(执行)我们上面说过的onDraw()方法。
class TestHandler extends Handler { @Override public void handleMessage(Message msg) { int temp = msg.what; //每设置一次setProgress的值就会调用onDraw方法 setProgress(temp); } }
由于execute是在循环里面,如果循环currentVideoStartPosition < getMax()成立并且if (running && !waiting)条件语句成立。
execute方法都会不停的被调用。进度条也会一直在移动(绘制)。
4、提供线程的相关操作。
//开始线程 public void start() { running = true; executorService.execute(thread); } //挂起线程 public void suspend() { if (waiting) { return; } synchronized (this) { this.waiting = true; } } //恢复线程 public void resume() { if (!waiting) { return; } synchronized (this) { this.waiting = false; this.notifyAll(); } } //停止线程 public void stop() { if (!running) { return; } synchronized (this) { running = false; } }
在这个内部类里面我向外面提供了四种方法来操作线程。
这些方法都比较简单。我就略过啦!
四、CustomProgressBar线程功能扩展
写完了内部类之后,此时的内部类已经具备了操作线程状态的能力。
现在为了让用户更方便的操作线程。咋们还需要回到CustomProgreeBar类中再次封装内部类的方法。方便外界直接调用。
在CustomProgreeBar类中写下下面五个方法。
/** * 开始进度条 * * @param videoView * @param currentVideoIndex */ public void startProgress(VideoView videoView, int currentVideoIndex) { if (currentVideoIndex > videoSum) { Log.e("-------------------->", "当前视频下标不能大于视频总数"); return; } customProgressBarRunnable = new CustomProgressBarRunnable(new TestHandler(), videoView, currentVideoIndex); //开启一个线程更新进度条 customProgressBarRunnable.start(); startDrawProgress = true; } public void hangUpThread() { if (customProgressBarRunnable == null) { return; } customProgressBarRunnable.suspend(); Log.e("-------------------->", "挂起线程!"); } public void recoverThread() { if (customProgressBarRunnable == null) { return; } customProgressBarRunnable.resume(); Log.e("-------------------->", "恢复线程!"); } public void stopThread() { if (customProgressBarRunnable == null) { return; } customProgressBarRunnable.stop(); Log.e("-------------------->", "停止线程!"); } /** * 关闭线程池 */ public void closeThreadPool() { executorService.shutdown(); }
startProgress从名字就可以看出这是一个开始进度条的方法。当我们点击视频的播放按钮的同时也要调用此方法开始绘制进度条
最后一个closeThreadPool关闭线程池,当播放视频的页面被销毁的时候,通过onDestory回调方法调用关闭线程池。
其它的三个方法从Log中可以看出是挂起,恢复,停止线程等操作。
五、CustomProgressBar控件的使用
1、创建一个名为activity_main的xml文件。具体代码如下所示。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#F2F2F2" android:orientation="vertical"> <TextView android:id="@+id/txt_bar_title" android:layout_width="match_parent" android:layout_height="40dp" android:layout_centerInParent="true" android:background="#33C774" android:gravity="center_horizontal|center_vertical" android:text="视频" android:textColor="#ffffff" android:textSize="16dp" /> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <VideoView android:id="@+id/video_view" android:layout_width="match_parent" android:layout_height="200dp" android:layout_centerHorizontal="true" android:layout_gravity="center_horizontal" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10dp" android:text="作者:Edward" android:textSize="16dp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="Android自定义控件---继承ProgressBar功能扩展\n" android:textSize="14dp" /> <TextView android:id="@+id/txt_video_number" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="2/15" android:textSize="14dp" /> <per.edward.ui.CustomProgressBar android:id="@+id/progressbar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:id="@+id/image_prev_video" android:layout_width="50dp" android:layout_height="50dp" android:layout_centerVertical="true" android:layout_marginRight="30dp" android:layout_toLeftOf="@+id/image_play_stop" android:onClick="onClick" android:src="@mipmap/video_left" /> <ImageView android:id="@+id/image_play_stop" android:layout_width="100dp" android:layout_height="100dp" android:layout_centerInParent="true" android:onClick="onClick" android:src="@mipmap/video_play" /> <ImageView android:id="@+id/image_netx_video" android:layout_width="50dp" android:layout_height="50dp" android:layout_centerVertical="true" android:layout_marginLeft="30dp" android:layout_toRightOf="@+id/image_play_stop" android:onClick="onClick" android:src="@mipmap/video_right" /> </RelativeLayout> </LinearLayout> </ScrollView> </LinearLayout>
xml文件预览图
为了让整个demo尽量简单,我删除了很多没用的布局,现在的这个布局和我在前面展示的布局不太一样。
需要特别注意的地方是在xml中使用自定用控件,应该用<包名路径.自定义控类名 />的方式。
例如CustomProgressBar的控件是在per.edward.ui目录下的类。
因此在xml文件使用的时候就是
<per.edward.ui.CustomProgressBar android:id="@+id/progressbar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" />
另外,在使用CustomProgressBar时,其默认形式是一个圆形进度条(因为这个类是继承ProgressBar的)。
其中style="?android:attr/progressBarStyleHorizontal"代码的意思是将一个圆形进度条改为水平形式。
2、首先在res的目录下创建一个名为raw的文件夹。将准备好的小视频放在src/main/res/raw目录里面。
如下图:
一般Android中都支持MP4,3gp等视频格式。如果你所放的视频格式不支持,可以去找软件转换格式。
3、XML和视频准备好之后。创建一个名为MainActivity的类并且让它继承Activity。
/** * 播放视频页面 * Created by Edward on 2016/4/15. */ public class MainActivity extends Activity { //视频视图实例 private VideoView videoView; //视频路径列表 private ArrayList<String> videoUriList; //咋们写的自定义控件 private CustomProgressBar progressBar; //播放视频按钮 private ImageView imagePlayStop; //用来显示当前视频下标以及视频的总数 private TextView txtVideoNumber; //当前视频的下标 private int currentVideoIndex = 0; //是否开启视频线程标记位 private boolean isOpenVideoThread = false; //获取内部类实例 private CustomProgressBar.CustomProgressBarRunnable customProgressBarRunnable; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); videoView = (VideoView) findViewById(R.id.video_view); videoUriList = new ArrayList<>(); progressBar = (CustomProgressBar) findViewById(R.id.progressbar); imagePlayStop = (ImageView) findViewById(R.id.image_play_stop); txtVideoNumber = (TextView) findViewById(R.id.txt_video_number); initData(); setCallBackListener(); } public void initData() { int[] videosId = {R.raw.bbb, R.raw.aaa, R.raw.ccc}; //获取raw文件夹内视频 for (int i = 0; i < videosId.length; i++) { videoUriList.add("android.resource://" + getPackageName() + "/" + videosId[i]); } //设置视频段数 progressBar.setVideoSum(videoUriList.size()); txtVideoNumber.setText(currentVideoIndex + 1 + "/" + videoUriList.size()); } /** * 设置回调监听事件 */ public void setCallBackListener() { //视频播放完毕之后回调此方法,关闭线程 videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { //将绘制进度条的线程暂停 progressBar.stopThread(); //将视频下标移动下一个 ++currentVideoIndex; //如果没有视频 if (currentVideoIndex > videoUriList.size() - 1) { Toast.makeText(MainActivity.this, "没有视频了", Toast.LENGTH_LONG).show(); //更换播放视频按钮的图标 imagePlayStop.setImageResource(R.mipmap.video_play); //将当前视频的下标设为0 currentVideoIndex = 0; //将开启线程的标记位设置false isOpenVideoThread = false; } else { //每当一个视频播放结束之后,再播放下一个视频。 openVideoThread(currentVideoIndex); txtVideoNumber.setText(currentVideoIndex + 1 + "/" + videoUriList.size()); } } }); videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { Toast.makeText(MainActivity.this, "视频播放错误!", Toast.LENGTH_LONG).show(); return false; } }); } public void onClick(View v) { switch (v.getId()) { //播放或暂停视频 case R.id.image_play_stop: imagePlayStop(); break; //下一个视频 case R.id.image_netx_video: ++currentVideoIndex; if (currentVideoIndex > videoUriList.size() - 1) { Toast.makeText(MainActivity.this, "没有视频了", Toast.LENGTH_LONG).show(); currentVideoIndex = videoUriList.size() - 1; } else { isOpenVideoThread = false; progressBar.stopThread(); imagePlayStop.setImageResource(R.mipmap.video_stop); txtVideoNumber.setText(currentVideoIndex + 1 + "/" + videoUriList.size()); openVideoThread(currentVideoIndex); } break; //上一个视频 case R.id.image_prev_video: --currentVideoIndex; if (currentVideoIndex < 0) { Toast.makeText(MainActivity.this, "没有视频了", Toast.LENGTH_LONG).show(); ++currentVideoIndex; } else { isOpenVideoThread = false; progressBar.stopThread(); imagePlayStop.setImageResource(R.mipmap.video_stop); txtVideoNumber.setText(currentVideoIndex + 1 + "/" + videoUriList.size()); openVideoThread(currentVideoIndex); } break; } } /** * 视频播放与暂停 */ public void imagePlayStop() { txtVideoNumber.setText(currentVideoIndex + 1 + "/" + videoUriList.size()); //如果没有开启视频(线程),那么就打开 if (!isOpenVideoThread) { imagePlayStop.setImageResource(R.mipmap.video_stop); openVideoThread(currentVideoIndex); } else { customProgressBarRunnable = progressBar.getCustomProgressBarRunnable(); //当前视频线程打开了之后,如果线程处于运行状态,那么就挂起,否则恢复线程。 if (!customProgressBarRunnable.isWaiting()) { //挂起进度条的线程 progressBar.hangUpThread(); //暂停视频播放 videoView.pause(); imagePlayStop.setImageResource(R.mipmap.video_play); } else { //恢复进度条的线程 progressBar.recoverThread(); //开始视频播放 videoView.start(); imagePlayStop.setImageResource(R.mipmap.video_stop); } } } /** * 开启视频线程 * * @param currentVideoIndex */ public void openVideoThread(int currentVideoIndex) { //把线程标记位设为已开启 isOpenVideoThread = true; //设置uri videoView.setVideoURI(Uri.parse(videoUriList.get(currentVideoIndex))); //开始视频播放 videoView.start(); //开启进度条绘制 progressBar.startProgress(videoView, currentVideoIndex); } @Override protected void onPause() { CustomProgressBar.CustomProgressBarRunnable customProgressBarRunnable = progressBar.getCustomProgressBarRunnable(); //当APP进入“不可见”状态的时候(例如返回桌面,锁屏,跳转到另一个Activity等情况),将视频暂停 if (customProgressBarRunnable != null && !customProgressBarRunnable.isWaiting()) imagePlayStop(); super.onPause(); } /** * 在该页面被销毁之前,关闭线程池,停止线程 */ @Override protected void onDestroy() { progressBar.closeThreadPool(); progressBar.stopThread(); super.onDestroy(); } }
onPause和onDestroy方法。这两个方法都属于Activity生命周期的回调方法。
- 从当前Activity返回桌面,锁屏,或者跳转到另一个Activity等情况。就会回调onPause方法,将当前正在播放的视频暂停(线程挂起)。
- 如果当前的Activity页面被销毁。就会回调onDestroy方法。关闭线程池和停止线程。
其它代码虽然比较多,但逻辑比较简单。无非就是“前进”,“后退”,“播放”三个按钮的操作。
以及数据的初始化,还有监听事件的回调等等,略过。
最后效果图
六、结束
这篇博客,写了好几天,这中间修修改改,为了减少博客的篇幅砍掉了很多代码和功能,将博客的重点放在自定义控件的讲解上。结果最后dem实现的效果与我给公司写的差别很大。不过没关系。写这篇博客主要目的就是为了总结一下近段时间学习Android自定义控件的成果,同时希望这一系列的博客能够帮助到各位。谢谢!
Demo源码请戳这里(由于里包含了视频资源所以整个文件比较大,)