SurfaceView实现手势绘制和视频播放

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zh_qianwei/article/details/49252107

原博客不更新 http://write.blog.csdn.net/postlist ,以后内容都会在这里写
在开始本文之前先看一段Log

“Skipped 47 frames! The application may be doing too much work on its main thread”

这个警告大多数是在自定义View时产生的,而其中在绘制过程中处理逻辑太多、刷新数据量比较大是主要原因
因为逻辑和刷新的数据一般和产品的效果或逻辑有关,优化的空间有限,如果对产品流程性要求很高就需要用新的东东来实现了。Android提供了SurfaceView就是来处理这种情形的。

View通过刷新来重绘视图,Android系统通过发出VSYNC信号来进行屏幕绘制,刷新时间间隔为16ms。如果18ms内View完成了你所须要执行的操作,那么在视觉上就不会参数卡顿;但是当执行逻辑或处理数据较多时,特别是需要频繁刷新的界面上,就会阻塞主线程导致画面卡顿。如:相机取景、视频播放、游戏界面绘制。普通应用最容易遇到的场景,在获取网络数据时加载动画,数据解析完毕隐藏动画显示UI。如果解析的数据量很大,在数据返回到解析完毕情况动画就会卡顿一下。

SurfaceView和View的主要区别:

  • View主要适用于主动更新,SurfaceView主要适用于被动、频繁更新
  • View在主线程中对画面进行刷,SurfaceView通常会通过一个子线程进行页面刷新
  • View在绘制是没有使用双缓冲机制,而SurfaceView在底层实现机制中已经实现了双缓冲机制

因为SurfaceView使用子线程更新页面,因此在有交互场景是会带来事件同步问题
SurfaceHolder的setType函数来设置绘制的类型,参数如下:
SURFACE_TYPE_NORMAL:用RAM缓存原生数据的普通Surface
SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access )引擎和硬件加速的Surface
SURFACE_TYPE_GPU:适用于GPU加速的Surface
SURFACE_TYPE_PUSH_BUFFERS:表明该Surface不包含原生数据,Surface用到的数据由其他对象提供,在Camera图像预览中就使用该类型的Surface,有Camera负责提供给预览Surface数据,这样图像预览会比较流畅。如果设置这种类型则就不能调用lockCanvas来获取Canvas对象了
下面是标准的SurfaceView的使用模板

class MyView extends SurfaceView implements SurfaceHolder.Callback,Runnable{  
        boolean isRunning; 
        SurfaceHolder holder;  
        Canvas canvas;
        public MyView(Context context) {  
            super(context);  
            initView();
        }  
      //初始化
       private void initView()
       {
        // TODO Auto-generated constructor stub  
            holder = getHolder();  
            holder.addCallback(this); 
            setFocusable(true); 
            setFocusableInTouchMode(true);
            this.setKeepScreenOn(true);
       }
       //SurfaceView创建
         @Override  
        public void surfaceCreated(SurfaceHolder holder) {  
            // TODO Auto-generated method stub 
            isRunning = true; 
            Thread t=new Thread(this);  
            t.start();  
        }  
       //SurfaceView改变
       @Override  
        public void surfaceChanged(SurfaceHolder holder, int format, int width,  
                int height) {  
            // TODO Auto-generated method stub  

        }  
       //SurfaceView销毁
        @Override  
        public void surfaceDestroyed(SurfaceHolder holder) {  
            // TODO Auto-generated method stub  
            isRunning = false;  
        }  
      // Runnable接口,Run方法的实现      
       @Override  
        public void run() {  
            while (isRunning) {  
                 onDraw();                  
            }  
        } 
        //绘制内容方法
        @Override  
        protected void onDraw() {  
             try { 
                   canvas = holder.lockCanvas();
                   //draw something
                } catch (InterruptedException e) {  

                } finally{//确保每次都能将内容提交
                  if(canvas != null)
                  {
                    holder.unlockCanvasAndPost(canvas);
                  }
                } 
        }  

    }  

下面来用SurfaceView实现二个具体的例子,一个是根据手势绘制内容,一个是播放视频

  • 手势的绘制
    下面是代码
public class SurfaceTouchView extends SurfaceView implements SurfaceHolder.Callback,Runnable{
    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用户绘制的Canvas
    private Canvas mCanvas;
    //子线程的标志位
    private boolean mIsRunning;
    //保存手指移动路径
    private Path mPath;
    private Paint mPaint;
    public SurfaceTouchView(Context context) {
        super(context);
        initView();
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mPath.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(x, y);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }
    private void initView()
    {
        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        this.setKeepScreenOn(true);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);//注意这行作用,可以去掉看看
        mPaint.setStrokeWidth(40);
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsRunning = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsRunning = false;
    }

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        while (mIsRunning)
        {
            draw();
        }
        long end = System.currentTimeMillis();
        //50-100
        long dutime = end - start;
        if (dutime < 100)
        {
            SystemClock.sleep(100-dutime);
        }
    }
    private void draw()
    {
        try {
            mCanvas = mHolder.lockCanvas();
            mCanvas.drawColor(Color.WHITE);
            mCanvas.drawPath(mPath,mPaint);
        }catch (Exception e)
        {

        }finally {
            if (mCanvas != null)
            {
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}

在Activity里面加一行

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SurfaceTouchView(this));
    }
}
  • 视频播放
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.SeekBar;
import android.widget.Toast;

import mfg.colormatrix.demo.R;

public class VideoPalyActivity extends AppCompatActivity {

    private final String TAG = "VideoPlay";
    private EditText et_path;
    private SurfaceView sv;
    private Button btn_play, btn_pause, btn_replay, btn_stop;
    private MediaPlayer mediaPlayer;
    private SeekBar seekBar;
    private int currentPosition = 0;
    private boolean isPlaying;
    private View notice;
    int index;
    String url = "你自己的视频地址" ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_paly);
        seekBar = (SeekBar) findViewById(R.id.seekBar);
        sv = (SurfaceView) findViewById(R.id.sv);
        notice = findViewById(R.id.notice);
        et_path = (EditText) findViewById(R.id.et_path);
        et_path.setText(url);
        btn_play = (Button) findViewById(R.id.btn_play);
        btn_pause = (Button) findViewById(R.id.btn_pause);
        btn_replay = (Button) findViewById(R.id.btn_replay);
        btn_stop = (Button) findViewById(R.id.btn_stop);

        btn_play.setOnClickListener(click);
        btn_pause.setOnClickListener(click);
        btn_replay.setOnClickListener(click);
        btn_stop.setOnClickListener(click);
        // 为SurfaceHolder添加回调
        sv.getHolder().addCallback(callback);

        // 4.0版本之下需要设置的属性
        // 设置Surface不维护自己的缓冲区,而是等待屏幕的渲染引擎将内容推送到界面
        sv.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        // 为进度条添加进度更改事件
        seekBar.setOnSeekBarChangeListener(change);
        new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                SystemClock.sleep(400);
                play(0);
            }
        }).start();

    }

    private SurfaceHolder.Callback callback = new Callback() {
        // SurfaceHolder被修改的时候回调
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            Log.i(TAG, "SurfaceHolder 被销毁");
            // 销毁SurfaceHolder的时候记录当前的播放位置并停止播放
            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                currentPosition = mediaPlayer.getCurrentPosition();
                mediaPlayer.stop();
            }
        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            Log.i(TAG, "SurfaceHolder 被创建");
            if (currentPosition > 0) {
                // 创建SurfaceHolder的时候,如果存在上次播放的位置,则按照上次播放位置进行播放
                play(currentPosition);
                currentPosition = 0;
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                                   int height) {
            Log.i(TAG, "SurfaceHolder 大小被改变");
        }

    };

    private SeekBar.OnSeekBarChangeListener change = new SeekBar.OnSeekBarChangeListener() {

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            // 当进度条停止修改的时候触发
            // 取得当前进度条的刻度
            int progress = seekBar.getProgress();
            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                // 设置当前播放的位置
                mediaPlayer.seekTo(progress);
            }
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {

        }

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress,
                                      boolean fromUser) {

        }
    };

    private View.OnClickListener click = new View.OnClickListener() {

        @Override
        public void onClick(View v) {

            switch (v.getId()) {
                case R.id.btn_play:
                    play(0);
                    break;
                case R.id.btn_pause:
                    pause();
                    break;
                case R.id.btn_replay:
                    replay();
                    break;
                case R.id.btn_stop:
                    stop();
                    break;
                default:
                    break;
            }
        }
    };


    /*
     * 停止播放
     */
    protected void stop() {
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.stop();
            mediaPlayer.release();
            mediaPlayer = null;
            btn_play.setEnabled(true);
            isPlaying = false;
        }
    }

    /**
     * 开始播放
     *
     * @param msec 播放初始位置
     */
    protected void play(final int msec) {
        /**注释内容是播放本地视频**/
//      获取视频文件地址
//      String path = et_path.getText().toString().trim();
//      File file = new File(path);
//      if (!file.exists()) {
//          Toast.makeText(this, "视频文件路径错误", 0).show();
//          return;
//      }
        try {
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            // 设置播放的视频源
            mediaPlayer.setDataSource(url);
            // 设置显示视频的SurfaceHolder
            mediaPlayer.setDisplay(sv.getHolder());
            Log.i(TAG, "开始装载");
            mediaPlayer.prepareAsync();
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {

                @Override
                public void onPrepared(MediaPlayer mp) {
                    seekBar.setVisibility(View.VISIBLE);
                    notice.setVisibility(View.GONE);
                    Log.i(TAG, "装载完成");
                    mediaPlayer.start();
                    // 按照初始位置播放
                    mediaPlayer.seekTo(msec);
                    // 设置进度条的最大进度为视频流的最大播放时长
                    seekBar.setMax(mediaPlayer.getDuration());
                    // 开始线程,更新进度条的刻度
                    new Thread() {

                        @Override
                        public void run() {
                            isPlaying = true;
                            while (isPlaying) {
                                int current = mediaPlayer
                                        .getCurrentPosition();
                                seekBar.setProgress(current);

                                SystemClock.sleep(500);
                            }
                        }
                    }.start();

                    btn_play.setEnabled(false);
                }
            });
            mediaPlayer.setOnCompletionListener(new OnCompletionListener() {

                @Override
                public void onCompletion(MediaPlayer mp) {
                    // 在播放完毕被回调
                    btn_play.setEnabled(true);
                }
            });

            mediaPlayer.setOnErrorListener(new OnErrorListener() {

                @Override
                public boolean onError(MediaPlayer mp, int what, int extra) {
                    // 发生错误重新播放
                    play(0);
                    isPlaying = false;
                    return false;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 重新开始播放
     */
    protected void replay() {
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.seekTo(0);
            Toast.makeText(this, "重新播放", Toast.LENGTH_SHORT).show();
            btn_pause.setText("暂停");
            return;
        }
        isPlaying = false;
        play(0);


    }

    /**
     * 暂停或继续
     */
    protected void pause() {
        if (btn_pause.getText().toString().trim().equals("继续")) {
            btn_pause.setText("暂停");
            mediaPlayer.start();
            Toast.makeText(this, "继续播放",  Toast.LENGTH_SHORT).show();
            return;
        }
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            btn_pause.setText("继续");
            Toast.makeText(this, "暂停播放",  Toast.LENGTH_SHORT).show();
        }

    }

}

xml代码如下,我这边为了调试把开始暂停布局隐藏了,需要的自己改一下即可

<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <EditText
        android:id="@+id/et_path"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="/这里可以写一个默认路径方便调试" />
    <RelativeLayout android:layout_width="match_parent"
        android:layout_height="300dp">
            <SurfaceView
        android:id="@+id/sv"
        android:layout_width="400dp"
        android:layout_height="300dp" />
    <TextView android:id="@+id/notice" android:layout_width="wrap_content"
        android:layout_height="wrap_content" android:text="视频正在加载中"
        android:textColor="@android:color/white"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"/>

    </RelativeLayout>

    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" 
        android:visibility="gone"    />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:visibility="gone" >

        <Button
            android:id="@+id/btn_play"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="播放" />

        <Button
            android:id="@+id/btn_pause"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="暂停" />

        <Button
            android:id="@+id/btn_replay"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="重播" />

        <Button
            android:id="@+id/btn_stop"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="停止" />
    </LinearLayout>


</LinearLayout>

在上面的例子当中我们启动线程都是这样的

new Thread(this).start();

我们可以用线程池来优化这部分,代码如下

扫描二维码关注公众号,回复: 5834139 查看本文章
  ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  singleThreadExecutor.execute(this);

这个singleThreadExecutor 对象可以写在Application里面,这样就无需频繁创建线程了,减少系统的资源消耗

猜你喜欢

转载自blog.csdn.net/zh_qianwei/article/details/49252107