多媒体编程

一、图片处理

BMP无损图片(无压缩)占用内存的大小 = 图片总像素 * 每个像素占用的大小

每个像素占用的大小
  • 单色:只有黑白两种颜色,使用0和1即可表示,每个像素只需要使用1个二进制位来表示颜色信息,占1/8字节。

  • 16色:用0 - 15(二进制0000 - 11111)来表示,每个像素需要使用4个二进制位来表示颜色信息,占1/2字节。

  • 256色:用0 - 255(二进制0000 0000 - 1111 1111)来表示,每个像素需要使用8个二进制位来表示颜色信息,占1个字节。

  • 24位色:每个像素占3个字节

    • R:0-255,占1个字节
    • G:0-255,占1个字节
    • B:0-255,占1个字节
加载大图片到内存

Android系统默认以ARGB_8888表示每个像素的颜色信息,所以每个像素占4个字节,很容易内存溢出(Out Of Memory, OOM)

如果图片尺寸大于屏幕尺寸,我们可以先缩小图片尺寸,再加载

/**
 * 图片原始尺寸2400 * 3200,总像素7680000
 * 屏幕尺寸320 * 480,总像素153600
 * 宽的缩放比例:2400 / 320 = 7
 * 高的缩放比例:3200 / 480 = 6
 * 两个比例不一样,取较大值7
 */
public void loadBigImage(String pathName) {

    // 获取图片宽高
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;  // 只请求图片边界(宽高),不解析图片像素(不为图片申请内存)
    BitmapFactory.decodeFile(pathName, options);// 返回null,获取的图片宽高保存在options中
    int imageWidth = options.outWidth;
    int imageHeight = options.outHeight;

    // 获取屏幕宽高
    Display display = getWindowManager().getDefaultDisplay();
    Point size = new Point();
    display.getSize(size);
    int screenWidth = size.x;
    int screenHeight = size.y;

    // 计算缩放比例
    int scale = 1;
    int scaleX = imageWidth / screenWidth;
    int scaleY = imageHeight / screenHeight;
    if (scaleX >= scaleY && scaleX > 1) {
        scale = scaleX;
    } else if (scaleX < scaleY && scaleY > 1) {
        scale = scaleY;
    }

    // 加载缩放后的图片
    options.inJustDecodeBounds = false; // 重新设置,解析图片像素(为图片申请内存)
    options.inSampleSize = scale; // 设置采样率(缩小的倍数),实际取值会向2的次方结算,例如inSampleSize=15,实际取值为8;inSampleSize=17,实际取值为16
    Bitmap bitmap = BitmapFactory.decodeFile(pathName, options);
    ImageView imageView = (ImageView) findViewById(R.id.iv);
    imageView.setImageBitmap(bitmap);
}
图片的简单变换

上面直接加载的Bitmap对象是只读的,无法修改,要修改图片只能先在内存中创建出一模一样的Bitmap副本,然后修改副本。

public void duangImage() {

    // 加载原图(为了方便把原图拷贝进项目中,而没有放在SD上)
    // 该Bitmap对象是只读的
    Bitmap bmpSrc = BitmapFactory.decodeResource(getResources(), R.drawable.image);
    ImageView iv_src = (ImageView) findViewById(R.id.iv_src);
    iv_src.setImageBitmap(bmpSrc);

    // 创建副本
    // 1.创建与原图一模一样大小的Bitmap对象,该对象当前是空白的,相当于一张与原图一样大小的白纸
    Bitmap bmpCopy = Bitmap.createBitmap(bmpSrc.getWidth(), bmpSrc.getHeight(), bmpSrc.getConfig());

    // 2.创建画笔
    Paint paint = new Paint();

    // 3.创建画板,把白纸铺到画板上,所有在该画板上的绘制都会画到这张白纸上
    Canvas canvas = new Canvas(bmpCopy);

    // 创建变换矩阵,变换的是图片而不是控件,所以图片做了变换很有可能出了控件
    Matrix matrix = new Matrix();

    // 平移效果
    matrix.setTranslate(20, 10); // 右移20dp,下移10dp

    // 旋转效果
    matrix.setRotate(45, bmpCopy.getWidth() / 2, bmpCopy.getHeight() / 2);   // 顺时针旋转45度,旋转中心设置在图片中央

    // 缩放效果
    matrix.setScale(2, 0.5f, bmpCopy.getWidth() / 2, bmpCopy.getHeight() / 2);// 宽度放大为2倍,高度缩小一半,缩放中心设置在图片中央

    // 镜面效果(水平翻转)
    matrix.setScale(-1, 1);     // 先将x坐标变为负
    matrix.postTranslate(bmpCopy.getWidth(), 0);// 再将图片右移图片宽度的距离(必须用postTranslate,因为setTranslate会先复位清空前面的变换效果)

    // 倒影效果(竖直翻转)
    matrix.setScale(1, -1);     // 先将y坐标变为负
    matrix.postTranslate(0, bmpCopy.getHeight());// 再将图片下移图片高度的距离

    // 4.作画
    canvas.drawBitmap(bmpSrc, matrix, paint);
    ImageView iv_copy = (ImageView) findViewById(R.id.iv_copy);
    iv_copy.setImageBitmap(bmpCopy);
}
手机画图板

手指在屏幕上触摸、滑动时记录触摸点的坐标,动态更新直线的起点和终点,实时绘制直线。

public class MainActivity extends AppCompatActivity {

    private ImageView iv;
    private Bitmap bmpSrc;
    private Bitmap bmpCopy;
    private Paint paint;
    private Canvas canvas;
    private int startX;
    private int startY;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bmpSrc = BitmapFactory.decodeResource(getResources(), R.drawable.bg);    // 原图
        bmpCopy = Bitmap.createBitmap(bmpSrc.getWidth(), bmpSrc.getHeight(), bmpSrc.getConfig());// 白纸
        paint = new Paint();                    // 画笔
        canvas = new Canvas(bmpCopy);           // 画板
        canvas.drawBitmap(bmpSrc, new Matrix(), paint); // 作画(拷贝)

        iv = (ImageView) findViewById(R.id.iv);
        iv.setOnTouchListener(new View.OnTouchListener() {
            // 用户手指只要触摸屏幕,就会产生触摸事件
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // 判断触摸事件的类型
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:    // 手指触摸屏幕
                        startX = (int) event.getX();
                        startY = (int) event.getY();
                        break;
                    case MotionEvent.ACTION_MOVE:     // 手指滑动
                        int newX = (int) event.getX();
                        int newY = (int) event.getY();
                        canvas.drawLine(startX, startY, newX, newY, paint);
                        iv.setImageBitmap(bmpCopy);
                        // 将本次画线的终点,设置为下一次画线的起点
                        startX = newX;
                        startY = newY;
                        break;
                    case MotionEvent.ACTION_UP:       // 手指离开屏幕

                        break;
                }
                // 子节点ImageView有优先选择权,子节点如果不处理(返回false,这个onTouch就不再调用),事件就交给父节点RelativeLayout处理
                // 返回true告诉系统,这个触摸事件由ImageView处理
                return true;
            }
        });
    }

    /**
     * 红色
     *
     * @param view
     */
    public void red(View view) {
        paint.setColor(Color.RED);
    }

    /**
     * 绿色
     *
     * @param view
     */
    public void green(View view) {
        paint.setColor(Color.GREEN);
    }

    /**
     * 刷子
     *
     * @param view
     */
    public void brush(View view) {
        paint.setStrokeWidth(8);    // 改变线条粗细
    }

    /**
     * 将绘制的图片保存到SD卡
     * 当图片保存到SD卡时,系统图库还查看不到这张图片,需要让系统重新遍历一次SD卡
     * 系统每次遍历SD卡,会为SD卡中的所有的多媒体文件(图片、音频、视频)生成一个索引
     * 索引中包含文件名、文件保存路径、标题、艺术家、持续时间等字段,这些索引被保存在MediaStore数据库中。
     * 用户每次启动系统图库时,图库应用并不会去遍历SD卡,而是直接从MediaStore数据库读取图片(和视频)的索引
     *
     * @param view
     */
    public void save(View view) {
        File file = new File("sdcard/mypaint.png");     // 保存路径
        FileOutputStream fos;
        try {
            fos = new FileOutputStream(file);
            bmpCopy.compress(Bitmap.CompressFormat.PNG, 100, fos); // 把bitmap压缩成一个png文件
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {   // Android 4.4以前,可以手动发送一个SD卡就绪广播通知系统扫描SD卡
            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_MEDIA_MOUNTED);
            intent.setData(Uri.fromFile(Environment.getExternalStorageDirectory()));
            sendBroadcast(intent);
        } else {    // Android 4.4以后,只用系统应用才有权发送SD卡就绪广播,所以我们要手动扫描SD卡
            MediaScannerConnection.scanFile(this, new String[]{file.getAbsolutePath()}, null, null);
        }
    }
}
撕衣服小游戏

将内衣照和外衣照重叠放置,内衣照放在下面,用户在屏幕上滑动时,触摸的是外衣照。把手指经过屏幕坐标对应的像素点置为透明,下层的内衣照就显示出来了。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Bitmap bmpSrc = BitmapFactory.decodeResource(getResources(), R.drawable.bg_with_clothes_small);       // 外衣原图
        final Bitmap bmpCopy = Bitmap.createBitmap(bmpSrc.getWidth(), bmpSrc.getHeight(), bmpSrc.getConfig()); // 创建白纸
        Paint paint = new Paint();                      // 画笔
        Canvas canvas = new Canvas(bmpCopy);            // 画板
        canvas.drawBitmap(bmpSrc, new Matrix(), paint); // 作画(拷贝)
        final ImageView iv = (ImageView) findViewById(R.id.iv);
        iv.setImageBitmap(bmpCopy);

        // 获取屏幕宽高,计算图片与屏幕的比例,因为图片压缩后屏幕坐标需要修正
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        int screenWidth = size.x;
        int screenHeight = size.y;
        final double scaleX = (double) bmpCopy.getWidth() / (double) screenWidth;
        final double scaleY = (double) bmpCopy.getHeight() / (double) screenHeight;
        iv.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_MOVE) { // 手指滑动
                    // 获取滑动经过的屏幕坐标
                    double x = event.getX();
                    double y = event.getY();
                    // 如果图片尺寸大于屏幕尺寸,图片会被压缩,需要把屏幕坐标对应到压缩后的图片像素点
                    if (scaleX > 1.0) {
                        x = x * scaleX;
                    }
                    if (scaleY > 1.0) {
                        y = y * scaleY;
                    }
                    for (int i = -20; i <= 20; i++) {
                        for (int j = -20; j <= 20; j++) {
                            if (x + i >= 0 && x + i < bmpCopy.getWidth() &&
                                    y + j >= 0 && y + j < bmpCopy.getHeight() &&
                                    i * i + j * j <= 400) {
                                bmpCopy.setPixel((int) Math.round(x) + i, (int) Math.round(y) + j, Color.TRANSPARENT);// 将图片的像素点方圆20里置为透明色
                            }
                        }
                    }
                    iv.setImageBitmap(bmpCopy);
                }
                return true;
            }
        });
    }
}

二、音频播放

音乐播放器

实现音乐播放时,要保证在Activity销毁后音乐仍在后台播放,进程不会变成空进程而在内存不足时被系统杀死,必须要通过startService()启动一个负责在后台播放音乐的MusicService,从而把进程变成服务进程。而MusicService中的方法还需要被前台Activity所调用(例如当用户点击播放或暂停按钮要分别触发MusicService中的播放音乐和暂停音乐),又必须通过bindService()绑定MusicService,获取IBinder对象。因此需要用到上述两种方式混合启动MusicService,并且startService()必须在bindService()之前执行

混合启动服务时先startService(),再bindService();停止服务时先unbindService(),再stopService()

Android系统提供了MediaPlayer来方便开发者控制音频播放,下面这张MediaPlayer的状态图详细的展示了MediaPlayer的使用方法

参考市面上主流的音乐播放器,我们还可以使用拖动条SeekBar来实时的显示音乐的播放进度,并让用户可以通过拖动SeekBar来改变播放进度。不多说,上代码!

前台MainActivity代码如下

public class MainActivity extends AppCompatActivity
{

    private static SeekBar sb_progress_controller;
    private ControllerInterface ci;

    // 通过Handler接收Service发过来的进度更新消息
    public static Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            Bundle data = msg.getData();
            int duration = data.getInt("duration");
            int currentPosition = data.getInt("currentPosition");
            // 设置进度条显示进度
            sb_progress_controller.setMax(duration);
            sb_progress_controller.setProgress(currentPosition);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sb_progress_controller = (SeekBar) findViewById(R.id.sb_progress_controller);
        sb_progress_controller.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() // 设置SeekBar滑动侦听
        {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)  // 手指滑动
            {

            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar)   // 手指按下
            {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar)    // 手指抬起
            {
                ci.seekTo(seekBar.getProgress());   // 获取SeekBar的当前进度,设置给MediaPlayer的播放进度
            }
        });

        Intent intent = new Intent(this, MusicService.class);   // 混合启动MusicService
        startService(intent);                                      
        bindService(intent, new ServiceConnection()
        {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service)
            {
                ci = (ControllerInterface)service;  // 中间人
            }

            @Override
            public void onServiceDisconnected(ComponentName name)
            {

            }
        }, BIND_AUTO_CREATE);
    }

    public void play(View v)
    {
        ci.play();
    }

    public void pause(View v)
    {
        ci.pause();
    }

    public void continuePlay(View view)
    {
        ci.continuePlay();
    }
}

后台MusicService代码如下

public class MusicService extends Service
{

    private MediaPlayer mediaPlayer;
    private Timer timer;

    @Override
    public void onCreate()
    {
        super.onCreate();
        mediaPlayer = new MediaPlayer();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent)
    {
        return new MusicController();
    }

    /**
     * 中间人MusicController
     */
    class MusicController extends Binder implements ControllerInterface
    {

        @Override
        public void play()
        {
            MusicService.this.play();
        }

        @Override
        public void pause()
        {
            MusicService.this.pause();
        }

        @Override
        public void continuePlay()
        {
            MusicService.this.continuePlay();
        }

        @Override
        public void seekTo(int progress)
        {
            MusicService.this.seekTo(progress);
        }
    }

    /**
     * (从头)播放
     */
    public void play()
    {
        mediaPlayer.reset();
        try
        {
            mediaPlayer.setDataSource("sdcard/music.mp3"); // 播放本地音乐
            mediaPlayer.prepare();    // 同步准备
            mediaPlayer.start();      // 从头播放,因为前面调用了prepare()进行初始化
            addTimer();               // 使用Timer定时更新播放进度

//            mediaPlayer.setDataSource("http://sc1.111ttt.com/2016/1/01/21/194211205447.mp3");    // 播放本地音乐
//            mediaPlayer.prepareAsync();     // 异步准备,因为同步阻塞UI线程
//            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener()
//            {
//                @Override
//                public void onPrepared(MediaPlayer mp)
//                {
//                    mediaPlayer.start();    // 对于异步准备,要在准备完成后播放
//                    addTimer();             // 使用Timer定时更新播放进度
//                }
//            });
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    /**
     * 暂停播放
     */
    public void pause()
    {
        mediaPlayer.pause();
    }

    /**
     * 继续播放
     */
    public void continuePlay()
    {
        mediaPlayer.start();    // 继续播放,因为没有重新初始化
    }

    /**
     * 手动调节音乐播放进度
     *
     * @param progress
     */
    public void seekTo(int progress)
    {
        mediaPlayer.seekTo(progress);
    }

    /**
     * 定时更新音乐播放进度
     */
    public void addTimer()
    {
        if(timer == null)
        {
            timer = new Timer();
            timer.schedule(new TimerTask()
            {
                @Override
                public void run()   // 这个run()也是在子线程中执行的
                {
                    int duration = mediaPlayer.getDuration();               // 获取播放总时长
                    int currentPosition = mediaPlayer.getCurrentPosition(); // 获取当前播放进度

                    Message msg = MainActivity.handler.obtainMessage();
                    // 把数据封装至消息
                    Bundle data = new Bundle();
                    data.putInt("duration", duration);
                    data.putInt("currentPosition", currentPosition);
                    msg.setData(data);
                    // 发送消息,通知前台Activity更新播放进度
                    MainActivity.handler.sendMessage(msg);
                }
            }, 5, 500); // 计时任务开始5ms后,run方法开始执行,且保持每隔500ms执行一次
        }
    }

    @Override
    public void onDestroy()
    {
        mediaPlayer.stop();
        mediaPlayer.release(); // 销毁MediaPlayer
        if (timer != null)
        {
            timer.cancel();    // 销毁Timer
            timer = null;
        }
        super.onDestroy();
    }
}

三、视频播放

视频播放器

除了播放音频,MediaPlayer还可以用来播放视频,但由于MediaPlayer主要用于播放音频,因此它没有提供视频输出界面,此时就需要借助SurfaceView来显示MediaPlayer播放的视频。

SurfaceView使用了双缓冲技术(内存中有两个画布,A画布显示至屏幕,B画布在缓冲区中绘制下一帧画面,绘制完毕后B显示至屏幕,A在缓冲区中继续绘制下一帧画面),不会出现闪屏,适用于对画面实时刷新要求较高的场景。
原生的MediaPlayer只支持3GP和MP4格式的视频播放,如果要播放其他格式的视频,需要实现对应格式视频文件的编解码,可以使用开源免费音视频编解码器FFmpeg或Vitamio来完成。
SurfaceView是一个重量级组件,功能较多,但占用资源也多,所以它只有在可见时才会被系统创建,不可见时(按Home键)就会被销毁。我们可以通过给SurfaceHolder添加CallBack,侦听SurfaceView什么时候被创建,什么时候被销毁。
使用MediaPlayer + SurfaceView播放视频的代码如下

public class MainActivity extends AppCompatActivity
{

    private MediaPlayer mediaPlayer;
    private static int progressCache = 0;  // 设置成静态,只要进程没被杀死(即使按Back键销毁Activity)仍存在

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        SurfaceView sv_video_play = (SurfaceView) findViewById(R.id.sv_video_play);
        final SurfaceHolder surfaceHolder = sv_video_play.getHolder();  // 获取SurfaceView控制器
        surfaceHolder.setKeepScreenOn(true);    // 设置播放时保持屏幕常亮
        surfaceHolder.addCallback(new SurfaceHolder.Callback()
        {
            @Override
            public void surfaceCreated(SurfaceHolder holder)    // SurfaceView创建完成后回调
            {
                if(mediaPlayer == null)
                {
                    mediaPlayer = new MediaPlayer();
                    mediaPlayer.reset();
                    try
                    {
                        mediaPlayer.setDataSource("sdcard/movie.3gp");
                        mediaPlayer.setDisplay(surfaceHolder);// 设置视频显示在哪个SurfaceView,必须在SurfaceView创建完成后设置才有效
                        mediaPlayer.prepare();
                        mediaPlayer.seekTo(progressCache);    // 跳到上一次停止的地方继续播放
                        mediaPlayer.start();
                    }
                    catch (IOException e)
                    {
                        e.printStackTrace();
                    }
                }
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)// SurfaceView的格式或大小发生改变时回调
            {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder)      // SurfaceView销毁后回调
            {
                if(mediaPlayer != null)
                {
                    progressCache = mediaPlayer.getCurrentPosition();// 停止前先保存播放进度
                    mediaPlayer.stop();
                    mediaPlayer.release();
                    mediaPlayer = null;
                }
            }
        });
    }
}

上述使用MediaPlayer + SurfaceView播放视频的方式略显麻烦,如果需要控制视频播放还需要自己定义控制按钮并编写相应的逻辑操作。
幸运的是,Android系统为我们提供了VideoView组件来方便我们实现视频播放,与VideoView结合使用的还有一个MediaController类,它的作用是提供一个友好的图形控制界面,通过该控制界面来控制视频的播放。下面的代码示范了如何使用VideoView来播放视频

public class MainActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        VideoView vv_video_play = (VideoView) findViewById(R.id.vv_video_play);
        MediaController mediaController = new MediaController(this);    
        vv_video_play.setVideoPath("sdcard/movie.3gp");
        // 建立VideoView与MediaController之间的关联,这样就不需要开发者自己去控制视频的播放、暂停等了
        vv_video_play.setMediaController(mediaController);      
        mediaController.setMediaPlayer(vv_video_play);
        // 让VideoView获取焦点
        vv_video_play.requestFocus();   
    }
}

四、摄像头操作

  • 使用系统提供的Activity进行拍照和摄像

    • 启动拍照Activity

      Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);  // 隐式启动系统提供的拍照Activity
      File outputFile = new File(Environment.getExternalStorageDirectory(), "photo.jpg");// 设置照片的输出路径
      intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
      startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
      
    • 启动摄像Activity

      Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);  // 隐式启动系统提供的摄像Activity
      File outputFile = new File(Environment.getExternalStorageDirectory(), "video.3gp");// 设置视频的输出路径
      intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
      intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);  // 设置所录视频的质量,0代表低质量,1代表高质量
      startActivityForResult(intent, CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE);
      
  • 自定义拍照和摄像的应用

Android 5.0对拍照API进行了全新的设计,新增了android.hardware.camera2来取代之前的android.hardware.Camera,大幅提高了Android系统拍照的功能。
由于只有Android 5.0以上的的设备才能使用Camera2,在最小版本(minSdkVersion)升到21之前,我们还是要继续使用Camera。关于Camera的使用方法,请参考API Guides

你可能感兴趣的:(多媒体编程)